blob: 92c85144d01d4450e21505a84881f4457cb6a081 [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file
package main
import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
"github.com/google/subcommands"
"go.chromium.org/luci/auth"
"go.chromium.org/luci/auth/client/authcli"
)
// UploadCommand uploads a data file to a URL when executed.
type UploadCommand struct {
// The timeout for HTTP requests.
timeout time.Duration
// The URL to upload data to
url string
// LUCI flags used to parse command-line authentication options.
authFlags authcli.Flags
}
func (*UploadCommand) Name() string {
return "upload"
}
func (*UploadCommand) Usage() string {
return "upload [options] json_file"
}
func (*UploadCommand) Synopsis() string {
return "Uploads a JSON file to a URL"
}
func (cmd *UploadCommand) SetFlags(flags *flag.FlagSet) {
cmd.authFlags = authcli.Flags{}
cmd.authFlags.Register(flags, auth.Options{})
flags.DurationVar(&cmd.timeout, "timeout", 10*time.Second,
"Request timeout duration string. e.g. 12s or 1m")
flags.StringVar(&cmd.url, "url", "", "(required) The URL to upload data to")
}
func (cmd *UploadCommand) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
if f.NArg() < 1 {
fmt.Fprintln(os.Stderr, "missing input file")
return subcommands.ExitFailure
}
if f.NArg() != 1 {
fmt.Fprintln(os.Stderr, "too many positional arguments")
return subcommands.ExitFailure
}
if len(cmd.url) == 0 {
fmt.Fprintln(os.Stderr, "url is required")
return subcommands.ExitFailure
}
if cmd.timeout <= 0 {
fmt.Fprintf(os.Stderr, "timeout must be positive. Got %v\n", cmd.timeout)
return subcommands.ExitFailure
}
opts, err := cmd.authFlags.Options()
if err != nil {
fmt.Fprintln(os.Stderr, err)
return subcommands.ExitFailure
}
inputFile := f.Arg(0)
if err := uploadData(ctx, cmd.url, inputFile, cmd.timeout, opts); err != nil {
fmt.Fprintln(os.Stderr, err)
return subcommands.ExitFailure
}
return subcommands.ExitSuccess
}
func uploadData(ctx context.Context, catapultURL string, filepath string, timeout time.Duration, authOpts auth.Options) error {
requestBody, err := ioutil.ReadFile(filepath)
if err != nil {
return err
}
// Verify that the input data is actual JSON. Ideally we'd verify the
// structure of the data also but this isn't always practical. (For
// example, when parsing a dynamic schema such as Catapult's
// HistogramSet).
if !json.Valid(requestBody) {
return errors.New("input is not valid JSON")
}
// URL encode the request body. This is weird but the Catapult dashboard
// will reject the request because it expects a body of the form:
// `data=url-encoded-histogramset` instead of the more usual JSON body:
// `{data: ...}`.
params := url.Values{}
params.Set("data", string(requestBody))
content := params.Encode()
req, err := http.NewRequest(http.MethodPost, catapultURL, strings.NewReader(content))
if err != nil {
return fmt.Errorf("create request: %v", err.Error())
}
// The User-Agent header helps distinguish this uploader's entries from
// other entries in the Catapult logs. If left empty, Go inserts its
// own header "Go-http-client/XX" where XX is the version of the agent.
// This is indistinguishable from other Go http clients.
req.Header.Set("User-Agent", "Fuchsia-Uploader/1.0")
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Content-Length", strconv.Itoa(len(content)))
// The LUCI authenticator used to authenticate the request. Silent login
// prevents an interactive login session when running from CI bots.
authenticator := auth.NewAuthenticator(ctx, auth.SilentLogin, authOpts)
// Generate the OAuth token used to authenticate the request.
oauthToken, err := authenticator.GetAccessToken(time.Minute)
if err != nil {
return fmt.Errorf("create token: %v", err.Error())
}
// Set an auth header on the request, containing the token generated from the
// provided service account information.
oauthToken.SetAuthHeader(req)
client, err := authenticator.Client()
if err != nil {
return err
}
client.Timeout = timeout
response, err := client.Do(req)
if err != nil {
return err
}
if response.StatusCode < 200 || response.StatusCode >= 300 {
return errors.New(response.Status)
}
log.Println(response.Status)
return nil
}