Snap for 12740440 from 60f7c64b7a22a30065e034ac1ed9e5bdc0d47c68 to androidx-activity-release

Change-Id: Ibbde0a2a4132ac56a342560c0e90080712be2071
diff --git a/README.md b/README.md
index e23d01b..f9939fd 100644
--- a/README.md
+++ b/README.md
@@ -6,13 +6,18 @@
 ## Options
 
 * `target`: **Required** - The target you would like to download the artifact from.
-* `build_id`: **Required** - The build_id of the target to download the artifact from.
 * `artifact`: **Required** - The artifact to download.
-* `output`: *Optional* - If you would like the contents of the file to be written to a specific file
+* **Required**: either `build_id` or `branch`, but not both
+  * When only `build_id` is provided, the script would download the artifact from that `build_id`.
+  * When only `branch` is provided, the script would download the artifact from the last known good build of that `branch`.
+* `output`: *Optional* - If you would like the contents of the file to be written to a specific file.
+* `client_id`: *Optional* - If authorization is required to download the artifact, please set this parameter as your OAuth2.0 Client ID.
+* `secret`: *Optional* - If authorization is required to download the artifact, please set this parameter as your OAuth2.0 Client secret.
+* `port`: *Optional* - If you would like to specify the OAuth callback port to listen on. Default: 10502
+* `project_id`: *Optional* - The project id being used to access the fetch APIs.
 * `-`: *Optional* - If you would like the contents of the file to be written to stdout  (must be the last arg)
 
-
-## Example useage
+## Example usage
 
 ```
 fetch_artifact -target=aosp_arm64-userdebug -build_id=7000390 -artifact=COPIED
@@ -24,6 +29,24 @@
 fetch_artifact -target=aosp_arm64-userdebug -build_id=7000390 -artifact=COPIED -
 ```
 
+### Get the latest successful build's artifact without specifying a build_id
+```
+fetch_artifact -target=aosp_arm64-trunk_staging-userdebug -branch=aosp-main -artifact=COPIED
+```
+
+### Using OAuth to fetch restricted artifacts
+In this case, you might need to create an OAuth 2.0 Client ID for a web application and set the redirect URI to `http://localhost:<port>`(default port: 10502).
+
+```
+fetch_artifact -target=<restricted_target> -build_id=<id> -artifact=COPIED -client_id=<OAuth_client_id> -secret=<OAuth_client_secret>
+```
+
+If you are accessing the fetch APIs from a different project than your OAuth client, you will need to specify the `-project_id` flag:
+
+```
+fetch_artifact -target=<restricted_target> -build_id=<id> -artifact=COPIED -client_id=<OAuth_client_id> -secret=<OAuth_client_secret> -project_id=<project_id>
+```
+
 ## Development
 
 ### Building
diff --git a/fetch_artifact.go b/fetch_artifact.go
index 843864c..9370698 100644
--- a/fetch_artifact.go
+++ b/fetch_artifact.go
@@ -15,21 +15,140 @@
 package main
 
 import (
+	"context"
+	"encoding/json"
+	"errors"
 	"flag"
 	"fmt"
 	"io"
 	"io/ioutil"
+	"log"
 	"net/http"
+	"net/url"
 	"os"
 	"path"
+	"time"
+
+	"golang.org/x/oauth2"
+	"golang.org/x/oauth2/google"
 )
 
-var target = flag.String("target", "", "the target to fetch from")
-var buildID = flag.String("build_id", "", "the build id to fetch from")
-var artifact = flag.String("artifact", "", "the artifact to download")
-var output = flag.String("output", "", "the file name to save as")
+var (
+	target    = flag.String("target", "", "the target to fetch from")
+	buildID   = flag.String("build_id", "", "the build id to fetch from, can use '-branch' to get the latest passed build ID")
+	branch    = flag.String("branch", "", "the branch to fetch from, used when '-build_id' is not provided,\nit would fetch the latest successful build")
+	artifact  = flag.String("artifact", "", "the artifact to download")
+	output    = flag.String("output", "", "the file name to save as")
+	clientID  = flag.String("client_id", "", "[Optional] OAuth 2.0 Client ID. Must be used with '-secret'")
+	secret    = flag.String("secret", "", "[Optional] OAuth 2.0 Client Secret. Must be used with '-client_id'")
+	port      = flag.Int("port", 10502, "[Optional] the port number where the oauth callback server would listen on")
+	projectID = flag.String("project_id", "", "[Optional] the project id being used to access the fetch APIs.")
+)
+
 var writeToStdout = false
 
+type BuildResponse struct {
+	Builds []Build `json:"builds"`
+}
+
+type Build struct {
+	BuildId string `json:"buildId"`
+}
+
+type Auth struct {
+	clientID string
+	secret   string
+}
+
+func newAuth(clientID, secret string) *Auth {
+	return &Auth{clientID: clientID, secret: secret}
+}
+
+func newClient(ctx context.Context, auth *Auth) *http.Client {
+	if auth == nil {
+		return &http.Client{}
+	}
+
+	config := &oauth2.Config{
+		ClientID:     auth.clientID,
+		ClientSecret: auth.secret,
+		Endpoint:     google.Endpoint,
+		Scopes:       []string{"https://www.googleapis.com/auth/androidbuild.internal"},
+	}
+	return newOAuthClient(ctx, config)
+}
+
+type FetchConfig struct {
+	client    *http.Client
+	target    string
+	buildID   string
+	branch    string
+	output    string
+	artifact  string
+	projectID string
+	args      []string
+}
+
+func newFetchConfig(client *http.Client, target string, buildID string, branch string, output string, artifact string, projectID string, args []string) (*FetchConfig, error) {
+	config := &FetchConfig{
+		client:    client,
+		target:    target,
+		buildID:   buildID,
+		branch:    branch,
+		output:    output,
+		artifact:  artifact,
+		projectID: projectID,
+		args:      args,
+	}
+	err := config.validate()
+	if err != nil {
+		return nil, err
+	}
+	return config, nil
+}
+
+func (c *FetchConfig) validate() error {
+	// We only support passing 1 argument `-` so if we have more than
+	// one argument this is an error state,
+	if len(c.args) > 1 {
+		return errors.New("too many arguments passed to fetch_artifact")
+	}
+
+	if len(c.args) > 0 {
+		writeToStdout = c.args[len(c.args)-1] == "-"
+		if !writeToStdout {
+			return fmt.Errorf(
+				"only supported final argument to fetch_artifact is `-` but got `%s`", c.args[len(c.args)-1])
+		}
+
+		if len(c.output) > 0 && writeToStdout {
+			return errors.New("both '-output' and '-' flags can not be used together")
+		}
+	}
+
+	// check user provided flags
+	if len(c.target) == 0 {
+		return errors.New("missing target")
+	}
+	if len(c.artifact) == 0 {
+		return errors.New("missing artifact")
+	}
+	if len(c.buildID) == 0 && len(c.branch) == 0 {
+		return errors.New("missing build_id or branch")
+	}
+	if len(c.buildID) != 0 && len(c.branch) != 0 {
+		return errors.New("too many arguments, you should only give build_id or branch")
+	}
+	if len(c.buildID) == 0 {
+		bid, err := getLatestGoodBuild(c)
+		if err != nil {
+			return err
+		}
+		c.buildID = bid
+	}
+	return nil
+}
+
 func errPrint(msg string) {
 	fmt.Fprintln(os.Stderr, msg)
 	os.Exit(1)
@@ -38,36 +157,106 @@
 func main() {
 	flag.Parse()
 	args := flag.Args()
-	// We only support passing 1 argument `-` so if we have more than
-	// one argument this is an error state,
-	if len(args) > 1 {
-		errPrint("Error: Too many arguments passed to fetch_artifact.")
+
+	var auth *Auth
+	if len(*clientID) != 0 && len(*secret) != 0 {
+		auth = newAuth(*clientID, *secret)
+	} else if len(*clientID) != 0 || len(*secret) != 0 {
+		errPrint(fmt.Sprintf("missing client_id or client_secret."))
 	}
 
-	if len(args) > 0 {
-		writeToStdout = args[len(args)-1] == "-"
-		if !writeToStdout {
-			errPrint(fmt.Sprintf(
-				"Error: Only supported final argument to fetch_artifact is `-` but got `%s`.", args[len(args)-1]))
-		}
+	ctx := context.Background()
+	client := newClient(ctx, auth)
 
-		if len(*output) > 0 && writeToStdout {
-			errPrint("Error: Both '-output' and '-' flags can not be used together.")
-		}
+	config, err := newFetchConfig(client, *target, *buildID, *branch, *output, *artifact, *projectID, args)
+	if err != nil {
+		errPrint(fmt.Sprintf("Config validation error: %s", err))
 	}
 
-	url := fmt.Sprintf("https://androidbuildinternal.googleapis.com/android/internal/build/v3/builds/%s/%s/attempts/latest/artifacts/%s/url", *buildID, *target, *artifact)
+	fetchArtifact(config)
+}
 
+func newOAuthClient(ctx context.Context, config *oauth2.Config) *http.Client {
+	token := tokenFromWeb(ctx, config)
+	return config.Client(ctx, token)
+}
+
+func tokenFromWeb(ctx context.Context, config *oauth2.Config) *oauth2.Token {
+	ch := make(chan string)
+	randState := fmt.Sprintf("st%d", time.Now().UnixNano())
+	ts := createServer(ch, randState)
+	go func() {
+		err := ts.ListenAndServe()
+		if err != http.ErrServerClosed {
+			errPrint(fmt.Sprintf("Listen and serve error: %v", err))
+		}
+	}()
+
+	defer ts.Close()
+	config.RedirectURL = "http://localhost" + ts.Addr
+	authURL := config.AuthCodeURL(randState)
+	log.Printf("Authorize this app at: %s", authURL)
+	code := <-ch
+
+	token, err := config.Exchange(ctx, code)
+	if err != nil {
+		errPrint(fmt.Sprintf("Token exchange error: %v", err))
+
+	}
+	return token
+}
+
+func createServer(ch chan string, state string) *http.Server {
+	return &http.Server{
+		Addr:    fmt.Sprintf(":%d", *port),
+		Handler: http.HandlerFunc(handler(ch, state)),
+	}
+}
+
+func handler(ch chan string, randState string) func(http.ResponseWriter, *http.Request) {
+	return func(rw http.ResponseWriter, req *http.Request) {
+		if req.URL.Path == "/favicon.ico" {
+			http.Error(rw, "error: visiting /favicon.ico", 404)
+			return
+		}
+		if req.FormValue("state") != randState {
+			log.Printf("state: %s doesn't match. (expected: %s)", req.FormValue("state"), randState)
+			http.Error(rw, "invalid state", 500)
+			return
+		}
+		if code := req.FormValue("code"); code != "" {
+			fmt.Fprintf(rw, "<h1>Success</h1>Authorized.")
+			rw.(http.Flusher).Flush()
+			ch <- code
+			return
+		}
+		ch <- ""
+		http.Error(rw, "invalid code", 500)
+	}
+}
+
+func sendRequest(client *http.Client, url string) (*http.Response, error) {
 	req, err := http.NewRequest("GET", url, nil)
 	if err != nil {
-		errPrint(fmt.Sprintf("unable to build request %v", err))
+		return nil, err
 	}
 	req.Header.Set("Accept", "application/json")
 
-	client := http.Client{}
 	res, err := client.Do(req)
 	if err != nil {
-		errPrint(fmt.Sprintf("Unable to make request %s.", err))
+		return nil, err
+	}
+	return res, nil
+}
+
+func fetchArtifact(c *FetchConfig) error {
+	apiURL := fmt.Sprintf("https://androidbuildinternal.googleapis.com/android/internal/build/v3/builds/%s/%s/attempts/latest/artifacts/%s/url", url.QueryEscape(c.buildID), url.QueryEscape(c.target), url.QueryEscape(c.artifact))
+	if len(c.projectID) != 0 {
+		apiURL += fmt.Sprintf("?$userProject=%s", url.QueryEscape(c.projectID))
+	}
+	res, err := sendRequest(c.client, apiURL)
+	if err != nil {
+		return fmt.Errorf("error fetching artifact %w", err)
 	}
 	defer res.Body.Close()
 
@@ -78,19 +267,50 @@
 
 	if writeToStdout {
 		io.Copy(os.Stdout, res.Body)
-		return
+		return nil
 	}
 
-	fileName := *artifact
-	if len(*output) > 0 {
-		fileName = *output
+	fileName := c.artifact
+	if len(c.output) > 0 {
+		fileName = c.output
 	}
 
 	f, err := os.Create(path.Base(fileName))
 	if err != nil {
-		errPrint(fmt.Sprintf("Unable to create file %s.", err))
+		return fmt.Errorf("unable to create file %w", err)
 	}
 	defer f.Close()
 	io.Copy(f, res.Body)
 	fmt.Printf("File %s created.\n", f.Name())
+	return nil
+}
+
+func getLatestGoodBuild(c *FetchConfig) (string, error) {
+	apiURL := fmt.Sprintf("https://androidbuildinternal.googleapis.com/android/internal/build/v3/builds?branches=%s&buildAttemptStatus=complete&buildType=submitted&maxResults=1&successful=true&target=%s", url.QueryEscape(c.branch), url.QueryEscape(c.target))
+	if len(c.projectID) != 0 {
+		apiURL += fmt.Sprintf("&$userProject=%s", url.QueryEscape(c.projectID))
+	}
+	res, err := sendRequest(c.client, apiURL)
+	if err != nil {
+		return "", fmt.Errorf("send request error: %w", err)
+	}
+	defer res.Body.Close()
+
+	if res.Status != "200 OK" {
+		body, _ := ioutil.ReadAll(res.Body)
+		return "", fmt.Errorf("unable to get Build ID: %s\n %s", res.Status, body)
+	}
+
+	body, _ := io.ReadAll(res.Body)
+	var buildData BuildResponse
+	err = json.Unmarshal(body, &buildData)
+
+	if err != nil {
+		return "", fmt.Errorf("error parsing JSON: %w", err)
+	}
+	if len(buildData.Builds) == 0 {
+		return "", errors.New("error no build ID is found")
+	}
+
+	return buildData.Builds[0].BuildId, nil
 }
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..df28410
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,7 @@
+module fetch_artifact
+
+go 1.23
+
+require golang.org/x/oauth2 v0.23.0
+
+require cloud.google.com/go/compute/metadata v0.3.0 // indirect
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..9666e99
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,6 @@
+cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
+cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
+golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=