go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/frontend/raw_artifact.go (about) 1 // Copyright 2023 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package frontend 16 17 import ( 18 "net/http" 19 "net/url" 20 21 "go.chromium.org/luci/common/errors" 22 "go.chromium.org/luci/grpc/appstatus" 23 resultpb "go.chromium.org/luci/resultdb/proto/v1" 24 "go.chromium.org/luci/server/auth" 25 "go.chromium.org/luci/server/router" 26 ) 27 28 // buildRawArtifactHandler builds a raw artifact handler for the given path 29 // prefix. 30 // The handler makes it possible to have stable, shareable URLs for artifacts. 31 // 32 // We implement this in Milo because 33 // 1. ResultDB doesn't support cookie based authentication nor a signin page. 34 // 2. Most users should have already signed in to Milo. 35 // 36 // The route is expected to have the format: `${prefix}*artifactName` 37 // For example, if the prefix is "/raw-artifact/", the route should be 38 // "/raw-artifact/*artifactName". 39 func (s *HTTPService) buildRawArtifactHandler(prefix string) func(ctx *router.Context) error { 40 return func(ctx *router.Context) error { 41 // Use EscapedPath so we can obtain the undecoded artifact name. 42 path := ctx.Request.URL.EscapedPath() 43 // Read artifactName by removing the prefix. 44 // We cannot read the artifactName from ctx.Params.ByName("ArtifactName") 45 // because the value is decoded automatically, making it impossible to 46 // differentiate "a%2fb" and "a/b". 47 // This is also why the path prefix is required to build the raw artifact 48 // handler. 49 // Related: https://github.com/julienschmidt/httprouter/issues/284 50 artifactName := path[len(prefix):] 51 52 settings, err := s.GetSettings(ctx.Request.Context()) 53 if err != nil { 54 return errors.Annotate(err, "failed to get Milo's service settings").Err() 55 } 56 rdbClient, err := s.GetResultDBClient(ctx.Request.Context(), settings.Resultdb.Host, auth.AsSessionUser) 57 if err != nil { 58 return errors.Annotate(err, "failed to get ResultDB client").Err() 59 } 60 61 artifact, err := rdbClient.GetArtifact(ctx.Request.Context(), &resultpb.GetArtifactRequest{Name: artifactName}) 62 if err != nil { 63 return appstatus.GRPCifyAndLog(ctx.Request.Context(), err) 64 } 65 66 fetchUrl := artifact.FetchUrl 67 parsedFetchUrl, err := url.Parse(fetchUrl) 68 if err != nil { 69 return errors.Annotate(err, "failed to parse artifact.fetchUrl").Err() 70 } 71 fetchUrlQuery := parsedFetchUrl.Query() 72 // Copy query params from request to fetch URL. 73 requestQuery := ctx.Request.URL.Query() 74 // We use two for loops as a query keycan have multiple values. 75 for key, values := range requestQuery { 76 for _, value := range values { 77 fetchUrlQuery.Add(key, value) 78 } 79 } 80 parsedFetchUrl.RawQuery = fetchUrlQuery.Encode() 81 82 http.Redirect(ctx.Writer, ctx.Request, parsedFetchUrl.String(), http.StatusFound) 83 return nil 84 } 85 }