44package repo
55
66import (
7+ "crypto/hmac"
8+ "crypto/sha256"
9+ "encoding/base64"
710 "errors"
811 "fmt"
912 "net/http"
1013 "net/url"
1114 "strings"
15+ "time"
16+
17+ go_context "context"
1218
1319 actions_model "code.gitea.io/gitea/models/actions"
1420 "code.gitea.io/gitea/models/db"
1521 secret_model "code.gitea.io/gitea/models/secret"
1622 "code.gitea.io/gitea/modules/actions"
1723 "code.gitea.io/gitea/modules/httplib"
24+ "code.gitea.io/gitea/modules/log"
1825 "code.gitea.io/gitea/modules/setting"
1926 api "code.gitea.io/gitea/modules/structs"
2027 "code.gitea.io/gitea/modules/util"
@@ -1084,6 +1091,21 @@ func DeleteArtifact(ctx *context.APIContext) {
10841091 ctx .Error (http .StatusNotFound , "artifact not found" , fmt .Errorf ("artifact not found" ))
10851092}
10861093
1094+ func buildSignature (endp , expires string , artifactID int64 ) []byte {
1095+ mac := hmac .New (sha256 .New , setting .GetGeneralTokenSigningSecret ())
1096+ mac .Write ([]byte (endp ))
1097+ mac .Write ([]byte (expires ))
1098+ mac .Write ([]byte (fmt .Sprint (artifactID )))
1099+ return mac .Sum (nil )
1100+ }
1101+
1102+ func buildSigURL (ctx go_context.Context , endp string , artifactID int64 ) string {
1103+ expires := time .Now ().Add (60 * time .Minute ).Format ("2006-01-02 15:04:05.999999999 -0700 MST" )
1104+ uploadURL := strings .TrimSuffix (httplib .GuessCurrentAppURL (ctx ), "/" ) +
1105+ "/" + endp + "?sig=" + base64 .URLEncoding .EncodeToString (buildSignature (endp , expires , artifactID )) + "&expires=" + url .QueryEscape (expires )
1106+ return uploadURL
1107+ }
1108+
10871109// DownloadArtifact Downloads a specific artifact for a workflow run redirects to blob url.
10881110func DownloadArtifact (ctx * context.APIContext ) {
10891111 // swagger:operation GET /repos/{owner}/{repo}/actions/artifacts/{artifact_id}/zip repository downloadArtifact
@@ -1136,9 +1158,9 @@ func DownloadArtifact(ctx *context.APIContext) {
11361158 ctx .Error (http .StatusInternalServerError , err .Error (), err )
11371159 return
11381160 }
1139- repoName := ctx . Repo . Repository . FullName ()
1140- url := httplib . MakeAbsoluteURL (ctx , setting . AppSubURL + "/ api/v1/repos/"+ repoName + "/actions/artifacts/" + fmt .Sprintf ("%d" , art .ID )+ "/zip/raw" )
1141- ctx .Redirect (url , http .StatusFound )
1161+
1162+ rurl := buildSigURL (ctx , " api/v1/repos/"+ url . PathEscape ( ctx . Repo . Repository . OwnerName ) + "/" + url . PathEscape ( ctx . Repo . Repository . Name ) + "/ actions/artifacts/"+ fmt .Sprintf ("%d" , art .ID )+ "/zip/raw" , art . ID )
1163+ ctx .Redirect (rurl , http .StatusFound )
11421164 return
11431165 }
11441166 // v3 not supported due to not having one unique id
@@ -1147,34 +1169,26 @@ func DownloadArtifact(ctx *context.APIContext) {
11471169
11481170// DownloadArtifactRaw Downloads a specific artifact for a workflow run directly.
11491171func DownloadArtifactRaw (ctx * context.APIContext ) {
1150- // swagger:operation GET /repos/{owner}/{repo}/actions/artifacts/{artifact_id}/zip/raw repository downloadArtifactRaw
1151- // ---
1152- // summary: Downloads a specific artifact for a workflow run directly
1153- // produces:
1154- // - application/json
1155- // parameters:
1156- // - name: owner
1157- // in: path
1158- // description: name of the owner
1159- // type: string
1160- // required: true
1161- // - name: repo
1162- // in: path
1163- // description: name of the repository
1164- // type: string
1165- // required: true
1166- // - name: artifact_id
1167- // in: path
1168- // description: id of the artifact
1169- // type: string
1170- // required: true
1171- // responses:
1172- // "200":
1173- // description: the artifact content
1174- // "400":
1175- // "$ref": "#/responses/error"
1176- // "404":
1177- // "$ref": "#/responses/notFound"
1172+ username := ctx .PathParam ("username" )
1173+ reponame := ctx .PathParam ("reponame" )
1174+ artifactID := ctx .PathParamInt64 ("artifact_id" )
1175+ sig := ctx .Req .URL .Query ().Get ("sig" )
1176+ expires := ctx .Req .URL .Query ().Get ("expires" )
1177+ dsig , _ := base64 .URLEncoding .DecodeString (sig )
1178+
1179+ endp := "api/v1/repos/" + url .PathEscape (username ) + "/" + url .PathEscape (reponame ) + "/actions/artifacts/" + fmt .Sprintf ("%d" , artifactID ) + "/zip/raw"
1180+ expecedsig := buildSignature (endp , expires , artifactID )
1181+ if ! hmac .Equal (dsig , expecedsig ) {
1182+ log .Error ("Error unauthorized" )
1183+ ctx .Error (http .StatusUnauthorized , "Error unauthorized" , nil )
1184+ return
1185+ }
1186+ t , err := time .Parse ("2006-01-02 15:04:05.999999999 -0700 MST" , expires )
1187+ if err != nil || t .Before (time .Now ()) {
1188+ log .Error ("Error link expired" )
1189+ ctx .Error (http .StatusUnauthorized , "Error link expired" , nil )
1190+ return
1191+ }
11781192
11791193 art , ok := getArtifactByID (ctx )
11801194 if ! ok {
@@ -1210,7 +1224,8 @@ func getArtifactByID(ctx *context.APIContext) (*actions_model.ActionArtifact, bo
12101224 return nil , false
12111225 }
12121226 // if artifacts status is not uploaded-confirmed, treat it as not found
1213- if ! ok || art .RepoID != ctx .Repo .Repository .ID || art .OwnerID != ctx .Repo .Repository .OwnerID || art .Status != int64 (actions_model .ArtifactStatusUploadConfirmed ) && art .Status != int64 (actions_model .ArtifactStatusExpired ) {
1227+ // ctx.Repo.Repository is nil for the raw download endpoint that checked this already
1228+ if ! ok || ctx .Repo != nil && ctx .Repo .Repository != nil && (art .RepoID != ctx .Repo .Repository .ID || art .OwnerID != ctx .Repo .Repository .OwnerID ) || art .Status != int64 (actions_model .ArtifactStatusUploadConfirmed ) && art .Status != int64 (actions_model .ArtifactStatusExpired ) {
12141229 ctx .Error (http .StatusNotFound , "artifact not found" , fmt .Errorf ("artifact not found" ))
12151230 return nil , false
12161231 }
0 commit comments