| // 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 |
| } |