blob: b611bd0f2fd25abd2114b4696dbef2422893302a [file] [log] [blame]
// Copyright 2019 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package common
import (
"bytes"
"compress/zlib"
"context"
"encoding/json"
"fmt"
"html/template"
"net/http"
"net/url"
"regexp"
"sort"
"strconv"
"strings"
"google.golang.org/grpc/codes"
"go.chromium.org/luci/auth/identity"
buildbucketpb "go.chromium.org/luci/buildbucket/proto"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/grpc/grpcutil"
"go.chromium.org/luci/server/auth"
)
// MergeStrings merges multiple string slices together into a single slice,
// removing duplicates.
func MergeStrings(sss ...[]string) []string {
result := []string{}
seen := map[string]bool{}
for _, ss := range sss {
for _, s := range ss {
if seen[s] {
continue
}
seen[s] = true
result = append(result, s)
}
}
sort.Strings(result)
return result
}
// ObfuscateEmail converts a string containing email address email@address.com
// into email<junk>@address.com.
func ObfuscateEmail(email string) template.HTML {
email = template.HTMLEscapeString(email)
return template.HTML(strings.Replace(
email, "@", "<span style=\"display:none\">ohnoyoudont</span>@", -1))
}
// ShortenEmail shortens Google emails.
func ShortenEmail(email string) string {
return strings.Replace(email, "@google.com", "", -1)
}
// TagGRPC annotates some gRPC with Milo specific semantics, specifically:
// * Marks the error as Unauthorized if the user is not logged in,
// and the underlying error was a 403 or 404.
// * Otherwise, tag the error with the original error code.
func TagGRPC(c context.Context, err error) error {
loggedIn := auth.CurrentIdentity(c) != identity.AnonymousIdentity
code := grpcutil.Code(err)
if code == codes.NotFound || code == codes.PermissionDenied {
// Mask the errors, so they look the same.
if loggedIn {
return errors.Reason("not found").Tag(grpcutil.NotFoundTag).Err()
}
return errors.Reason("not logged in").Tag(grpcutil.UnauthenticatedTag).Err()
}
return grpcutil.ToGRPCErr(err)
}
// ParseIntFromForm parses an integer from a form.
func ParseIntFromForm(form url.Values, key string, base int, bitSize int) (int64, error) {
input, err := ReadExactOneFromForm(form, key)
if err != nil {
return 0, err
}
ret, err := strconv.ParseInt(input, 10, 64)
if err != nil {
return 0, errors.Annotate(err, "invalid %v; expected an integer; actual value: %v", key, input).Err()
}
return ret, nil
}
// ReadExactOneFromForm read a string from a form.
// There must be exactly one and non-empty entry of the given key in the form.
func ReadExactOneFromForm(form url.Values, key string) (string, error) {
input := form[key]
if len(input) != 1 || input[0] == "" {
return "", fmt.Errorf("multiple or missing %v; actual value: %v", key, input)
}
return input[0], nil
}
// LegacyBuilderIDString returns a legacy string identifying the builder.
// It is used in the Milo datastore.
func LegacyBuilderIDString(bid *buildbucketpb.BuilderID) string {
return fmt.Sprintf("buildbucket/luci.%s.%s/%s", bid.Project, bid.Bucket, bid.Builder)
}
var ErrInvalidLegacyBuilderID = errors.New("the string is not a valid legacy builder ID")
var legacyBuilderIDRe = regexp.MustCompile(`^buildbucket/luci\.([^./]+)\.([^./]+)/([^./]+)$`)
// ParseLegacyBuilderID parses the legacy builder ID
// (e.g. `buildbucket/luci.<project>.<bucket>/<builder>`)
// and returns the builder ID in the new format.
func ParseLegacyBuilderID(bid string) (*buildbucketpb.BuilderID, error) {
match := legacyBuilderIDRe.FindStringSubmatch(bid)
if len(match) == 0 {
return nil, ErrInvalidLegacyBuilderID
}
return &buildbucketpb.BuilderID{
Project: match[1],
Bucket: match[2],
Builder: match[3],
}, nil
}
var legacyBuildIDWithBuildNumRe = regexp.MustCompile(`^buildbucket/luci\.([^./]+)\.([^./]+)/([^./]+)/(\d+)$`)
var ErrInvalidLegacyBuildID = errors.New("the string is not a valid legacy build ID")
// ParseLegacyBuildID parses the legacy build ID
// (e.g. `buildbucket/luci.<project>.<bucket>/<builder>/<number>`)
func ParseLegacyBuildID(bid string) (builderID *buildbucketpb.BuilderID, number int32, err error) {
match := legacyBuildIDWithBuildNumRe.FindStringSubmatch(bid)
if len(match) == 0 {
return nil, 0, ErrInvalidLegacyBuildID
}
builderID = &buildbucketpb.BuilderID{
Project: match[1],
Bucket: match[2],
Builder: match[3],
}
buildNum, err := strconv.ParseInt(match[4], 10, 32)
if err != nil {
return nil, 0, ErrInvalidLegacyBuildID
}
return builderID, int32(buildNum), nil
}
// JSONMarshalCompressed converts a message into compressed JSON form, suitable for storing in memcache.
func JSONMarshalCompressed(message interface{}) ([]byte, error) {
// Compress using zlib.
b := bytes.Buffer{}
w := zlib.NewWriter(&b)
enc := json.NewEncoder(w)
if err := enc.Encode(message); err != nil {
return nil, err
}
// Flush remaining bytes in the zlib writer.
if err := w.Close(); err != nil {
return nil, err
}
return b.Bytes(), nil
}
// JSONUnmarshalCompressed converts a message back from compressed JSON form.
func JSONUnmarshalCompressed(serialized []byte, out interface{}) error {
// Decompress using zlib.
r, err := zlib.NewReader(bytes.NewReader(serialized))
if err != nil {
return err
}
dec := json.NewDecoder(r)
return dec.Decode(out)
}
// GetJSONData fetches data from the given URL, parses the response body to `out`.
// It follows redirection and returns an error if the status code is 4xx or 5xx.
func GetJSONData(client *http.Client, url string, out interface{}) (err error) {
response, err := client.Get(url)
if err != nil {
return err
}
defer func() {
err = errors.Flatten(errors.NewMultiError(response.Body.Close(), err))
}()
if response.StatusCode >= 400 && response.StatusCode <= 599 {
return fmt.Errorf("failed to fetch data: %q returned code %q", url, response.Status)
}
dec := json.NewDecoder(response.Body)
return dec.Decode(out)
}