code.gitea.io/gitea@v1.21.7/routers/web/repo/githttp.go (about) 1 // Copyright 2014 The Gogs Authors. All rights reserved. 2 // Copyright 2019 The Gitea Authors. All rights reserved. 3 // SPDX-License-Identifier: MIT 4 5 package repo 6 7 import ( 8 "bytes" 9 "compress/gzip" 10 gocontext "context" 11 "fmt" 12 "net/http" 13 "os" 14 "path" 15 "regexp" 16 "strconv" 17 "strings" 18 "sync" 19 "time" 20 21 actions_model "code.gitea.io/gitea/models/actions" 22 auth_model "code.gitea.io/gitea/models/auth" 23 "code.gitea.io/gitea/models/perm" 24 access_model "code.gitea.io/gitea/models/perm/access" 25 repo_model "code.gitea.io/gitea/models/repo" 26 "code.gitea.io/gitea/models/unit" 27 "code.gitea.io/gitea/modules/context" 28 "code.gitea.io/gitea/modules/git" 29 "code.gitea.io/gitea/modules/log" 30 repo_module "code.gitea.io/gitea/modules/repository" 31 "code.gitea.io/gitea/modules/setting" 32 "code.gitea.io/gitea/modules/structs" 33 "code.gitea.io/gitea/modules/util" 34 repo_service "code.gitea.io/gitea/services/repository" 35 36 "github.com/go-chi/cors" 37 ) 38 39 func HTTPGitEnabledHandler(ctx *context.Context) { 40 if setting.Repository.DisableHTTPGit { 41 ctx.Resp.WriteHeader(http.StatusForbidden) 42 _, _ = ctx.Resp.Write([]byte("Interacting with repositories by HTTP protocol is not allowed")) 43 } 44 } 45 46 func CorsHandler() func(next http.Handler) http.Handler { 47 if setting.Repository.AccessControlAllowOrigin != "" { 48 return cors.Handler(cors.Options{ 49 AllowedOrigins: []string{setting.Repository.AccessControlAllowOrigin}, 50 AllowedHeaders: []string{"Content-Type", "Authorization", "User-Agent"}, 51 }) 52 } 53 return func(next http.Handler) http.Handler { 54 return next 55 } 56 } 57 58 // httpBase implementation git smart HTTP protocol 59 func httpBase(ctx *context.Context) *serviceHandler { 60 username := ctx.Params(":username") 61 reponame := strings.TrimSuffix(ctx.Params(":reponame"), ".git") 62 63 if ctx.FormString("go-get") == "1" { 64 context.EarlyResponseForGoGetMeta(ctx) 65 return nil 66 } 67 68 var isPull, receivePack bool 69 service := ctx.FormString("service") 70 if service == "git-receive-pack" || 71 strings.HasSuffix(ctx.Req.URL.Path, "git-receive-pack") { 72 isPull = false 73 receivePack = true 74 } else if service == "git-upload-pack" || 75 strings.HasSuffix(ctx.Req.URL.Path, "git-upload-pack") { 76 isPull = true 77 } else if service == "git-upload-archive" || 78 strings.HasSuffix(ctx.Req.URL.Path, "git-upload-archive") { 79 isPull = true 80 } else { 81 isPull = ctx.Req.Method == "GET" 82 } 83 84 var accessMode perm.AccessMode 85 if isPull { 86 accessMode = perm.AccessModeRead 87 } else { 88 accessMode = perm.AccessModeWrite 89 } 90 91 isWiki := false 92 unitType := unit.TypeCode 93 var wikiRepoName string 94 if strings.HasSuffix(reponame, ".wiki") { 95 isWiki = true 96 unitType = unit.TypeWiki 97 wikiRepoName = reponame 98 reponame = reponame[:len(reponame)-5] 99 } 100 101 owner := ctx.ContextUser 102 if !owner.IsOrganization() && !owner.IsActive { 103 ctx.PlainText(http.StatusForbidden, "Repository cannot be accessed. You cannot push or open issues/pull-requests.") 104 return nil 105 } 106 107 repoExist := true 108 repo, err := repo_model.GetRepositoryByName(owner.ID, reponame) 109 if err != nil { 110 if repo_model.IsErrRepoNotExist(err) { 111 if redirectRepoID, err := repo_model.LookupRedirect(owner.ID, reponame); err == nil { 112 context.RedirectToRepo(ctx.Base, redirectRepoID) 113 return nil 114 } 115 repoExist = false 116 } else { 117 ctx.ServerError("GetRepositoryByName", err) 118 return nil 119 } 120 } 121 122 // Don't allow pushing if the repo is archived 123 if repoExist && repo.IsArchived && !isPull { 124 ctx.PlainText(http.StatusForbidden, "This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.") 125 return nil 126 } 127 128 // Only public pull don't need auth. 129 isPublicPull := repoExist && !repo.IsPrivate && isPull 130 var ( 131 askAuth = !isPublicPull || setting.Service.RequireSignInView 132 environ []string 133 ) 134 135 // don't allow anonymous pulls if organization is not public 136 if isPublicPull { 137 if err := repo.LoadOwner(ctx); err != nil { 138 ctx.ServerError("LoadOwner", err) 139 return nil 140 } 141 142 askAuth = askAuth || (repo.Owner.Visibility != structs.VisibleTypePublic) 143 } 144 145 // check access 146 if askAuth { 147 // rely on the results of Contexter 148 if !ctx.IsSigned { 149 // TODO: support digit auth - which would be Authorization header with digit 150 ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea"`) 151 ctx.Error(http.StatusUnauthorized) 152 return nil 153 } 154 155 context.CheckRepoScopedToken(ctx, repo, auth_model.GetScopeLevelFromAccessMode(accessMode)) 156 if ctx.Written() { 157 return nil 158 } 159 160 if ctx.IsBasicAuth && ctx.Data["IsApiToken"] != true && ctx.Data["IsActionsToken"] != true { 161 _, err = auth_model.GetTwoFactorByUID(ctx, ctx.Doer.ID) 162 if err == nil { 163 // TODO: This response should be changed to "invalid credentials" for security reasons once the expectation behind it (creating an app token to authenticate) is properly documented 164 ctx.PlainText(http.StatusUnauthorized, "Users with two-factor authentication enabled cannot perform HTTP/HTTPS operations via plain username and password. Please create and use a personal access token on the user settings page") 165 return nil 166 } else if !auth_model.IsErrTwoFactorNotEnrolled(err) { 167 ctx.ServerError("IsErrTwoFactorNotEnrolled", err) 168 return nil 169 } 170 } 171 172 if !ctx.Doer.IsActive || ctx.Doer.ProhibitLogin { 173 ctx.PlainText(http.StatusForbidden, "Your account is disabled.") 174 return nil 175 } 176 177 environ = []string{ 178 repo_module.EnvRepoUsername + "=" + username, 179 repo_module.EnvRepoName + "=" + reponame, 180 repo_module.EnvPusherName + "=" + ctx.Doer.Name, 181 repo_module.EnvPusherID + fmt.Sprintf("=%d", ctx.Doer.ID), 182 repo_module.EnvAppURL + "=" + setting.AppURL, 183 } 184 185 if repoExist { 186 // Because of special ref "refs/for" .. , need delay write permission check 187 if git.SupportProcReceive { 188 accessMode = perm.AccessModeRead 189 } 190 191 if ctx.Data["IsActionsToken"] == true { 192 taskID := ctx.Data["ActionsTaskID"].(int64) 193 task, err := actions_model.GetTaskByID(ctx, taskID) 194 if err != nil { 195 ctx.ServerError("GetTaskByID", err) 196 return nil 197 } 198 if task.RepoID != repo.ID { 199 ctx.PlainText(http.StatusForbidden, "User permission denied") 200 return nil 201 } 202 203 if task.IsForkPullRequest { 204 if accessMode > perm.AccessModeRead { 205 ctx.PlainText(http.StatusForbidden, "User permission denied") 206 return nil 207 } 208 environ = append(environ, fmt.Sprintf("%s=%d", repo_module.EnvActionPerm, perm.AccessModeRead)) 209 } else { 210 if accessMode > perm.AccessModeWrite { 211 ctx.PlainText(http.StatusForbidden, "User permission denied") 212 return nil 213 } 214 environ = append(environ, fmt.Sprintf("%s=%d", repo_module.EnvActionPerm, perm.AccessModeWrite)) 215 } 216 } else { 217 p, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) 218 if err != nil { 219 ctx.ServerError("GetUserRepoPermission", err) 220 return nil 221 } 222 223 if !p.CanAccess(accessMode, unitType) { 224 ctx.PlainText(http.StatusNotFound, "Repository not found") 225 return nil 226 } 227 } 228 229 if !isPull && repo.IsMirror { 230 ctx.PlainText(http.StatusForbidden, "mirror repository is read-only") 231 return nil 232 } 233 } 234 235 if !ctx.Doer.KeepEmailPrivate { 236 environ = append(environ, repo_module.EnvPusherEmail+"="+ctx.Doer.Email) 237 } 238 239 if isWiki { 240 environ = append(environ, repo_module.EnvRepoIsWiki+"=true") 241 } else { 242 environ = append(environ, repo_module.EnvRepoIsWiki+"=false") 243 } 244 } 245 246 if !repoExist { 247 if !receivePack { 248 ctx.PlainText(http.StatusNotFound, "Repository not found") 249 return nil 250 } 251 252 if isWiki { // you cannot send wiki operation before create the repository 253 ctx.PlainText(http.StatusNotFound, "Repository not found") 254 return nil 255 } 256 257 if owner.IsOrganization() && !setting.Repository.EnablePushCreateOrg { 258 ctx.PlainText(http.StatusForbidden, "Push to create is not enabled for organizations.") 259 return nil 260 } 261 if !owner.IsOrganization() && !setting.Repository.EnablePushCreateUser { 262 ctx.PlainText(http.StatusForbidden, "Push to create is not enabled for users.") 263 return nil 264 } 265 266 // Return dummy payload if GET receive-pack 267 if ctx.Req.Method == http.MethodGet { 268 dummyInfoRefs(ctx) 269 return nil 270 } 271 272 repo, err = repo_service.PushCreateRepo(ctx, ctx.Doer, owner, reponame) 273 if err != nil { 274 log.Error("pushCreateRepo: %v", err) 275 ctx.Status(http.StatusNotFound) 276 return nil 277 } 278 } 279 280 if isWiki { 281 // Ensure the wiki is enabled before we allow access to it 282 if _, err := repo.GetUnit(ctx, unit.TypeWiki); err != nil { 283 if repo_model.IsErrUnitTypeNotExist(err) { 284 ctx.PlainText(http.StatusForbidden, "repository wiki is disabled") 285 return nil 286 } 287 log.Error("Failed to get the wiki unit in %-v Error: %v", repo, err) 288 ctx.ServerError("GetUnit(UnitTypeWiki) for "+repo.FullName(), err) 289 return nil 290 } 291 } 292 293 environ = append(environ, repo_module.EnvRepoID+fmt.Sprintf("=%d", repo.ID)) 294 295 w := ctx.Resp 296 r := ctx.Req 297 cfg := &serviceConfig{ 298 UploadPack: true, 299 ReceivePack: true, 300 Env: environ, 301 } 302 303 r.URL.Path = strings.ToLower(r.URL.Path) // blue: In case some repo name has upper case name 304 305 dir := repo_model.RepoPath(username, reponame) 306 if isWiki { 307 dir = repo_model.RepoPath(username, wikiRepoName) 308 } 309 310 return &serviceHandler{cfg, w, r, dir, cfg.Env} 311 } 312 313 var ( 314 infoRefsCache []byte 315 infoRefsOnce sync.Once 316 ) 317 318 func dummyInfoRefs(ctx *context.Context) { 319 infoRefsOnce.Do(func() { 320 tmpDir, err := os.MkdirTemp(os.TempDir(), "gitea-info-refs-cache") 321 if err != nil { 322 log.Error("Failed to create temp dir for git-receive-pack cache: %v", err) 323 return 324 } 325 326 defer func() { 327 if err := util.RemoveAll(tmpDir); err != nil { 328 log.Error("RemoveAll: %v", err) 329 } 330 }() 331 332 if err := git.InitRepository(ctx, tmpDir, true); err != nil { 333 log.Error("Failed to init bare repo for git-receive-pack cache: %v", err) 334 return 335 } 336 337 refs, _, err := git.NewCommand(ctx, "receive-pack", "--stateless-rpc", "--advertise-refs", ".").RunStdBytes(&git.RunOpts{Dir: tmpDir}) 338 if err != nil { 339 log.Error(fmt.Sprintf("%v - %s", err, string(refs))) 340 } 341 342 log.Debug("populating infoRefsCache: \n%s", string(refs)) 343 infoRefsCache = refs 344 }) 345 346 ctx.RespHeader().Set("Expires", "Fri, 01 Jan 1980 00:00:00 GMT") 347 ctx.RespHeader().Set("Pragma", "no-cache") 348 ctx.RespHeader().Set("Cache-Control", "no-cache, max-age=0, must-revalidate") 349 ctx.RespHeader().Set("Content-Type", "application/x-git-receive-pack-advertisement") 350 _, _ = ctx.Write(packetWrite("# service=git-receive-pack\n")) 351 _, _ = ctx.Write([]byte("0000")) 352 _, _ = ctx.Write(infoRefsCache) 353 } 354 355 type serviceConfig struct { 356 UploadPack bool 357 ReceivePack bool 358 Env []string 359 } 360 361 type serviceHandler struct { 362 cfg *serviceConfig 363 w http.ResponseWriter 364 r *http.Request 365 dir string 366 environ []string 367 } 368 369 func (h *serviceHandler) setHeaderNoCache() { 370 h.w.Header().Set("Expires", "Fri, 01 Jan 1980 00:00:00 GMT") 371 h.w.Header().Set("Pragma", "no-cache") 372 h.w.Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate") 373 } 374 375 func (h *serviceHandler) setHeaderCacheForever() { 376 now := time.Now().Unix() 377 expires := now + 31536000 378 h.w.Header().Set("Date", fmt.Sprintf("%d", now)) 379 h.w.Header().Set("Expires", fmt.Sprintf("%d", expires)) 380 h.w.Header().Set("Cache-Control", "public, max-age=31536000") 381 } 382 383 func containsParentDirectorySeparator(v string) bool { 384 if !strings.Contains(v, "..") { 385 return false 386 } 387 for _, ent := range strings.FieldsFunc(v, isSlashRune) { 388 if ent == ".." { 389 return true 390 } 391 } 392 return false 393 } 394 395 func isSlashRune(r rune) bool { return r == '/' || r == '\\' } 396 397 func (h *serviceHandler) sendFile(contentType, file string) { 398 if containsParentDirectorySeparator(file) { 399 log.Error("request file path contains invalid path: %v", file) 400 h.w.WriteHeader(http.StatusBadRequest) 401 return 402 } 403 reqFile := path.Join(h.dir, file) 404 405 fi, err := os.Stat(reqFile) 406 if os.IsNotExist(err) { 407 h.w.WriteHeader(http.StatusNotFound) 408 return 409 } 410 411 h.w.Header().Set("Content-Type", contentType) 412 h.w.Header().Set("Content-Length", fmt.Sprintf("%d", fi.Size())) 413 h.w.Header().Set("Last-Modified", fi.ModTime().Format(http.TimeFormat)) 414 http.ServeFile(h.w, h.r, reqFile) 415 } 416 417 // one or more key=value pairs separated by colons 418 var safeGitProtocolHeader = regexp.MustCompile(`^[0-9a-zA-Z]+=[0-9a-zA-Z]+(:[0-9a-zA-Z]+=[0-9a-zA-Z]+)*$`) 419 420 func prepareGitCmdWithAllowedService(service string, h *serviceHandler) (*git.Command, error) { 421 if service == "receive-pack" && h.cfg.ReceivePack { 422 return git.NewCommand(h.r.Context(), "receive-pack"), nil 423 } 424 if service == "upload-pack" && h.cfg.UploadPack { 425 return git.NewCommand(h.r.Context(), "upload-pack"), nil 426 } 427 428 return nil, fmt.Errorf("service %q is not allowed", service) 429 } 430 431 func serviceRPC(h *serviceHandler, service string) { 432 defer func() { 433 if err := h.r.Body.Close(); err != nil { 434 log.Error("serviceRPC: Close: %v", err) 435 } 436 }() 437 438 expectedContentType := fmt.Sprintf("application/x-git-%s-request", service) 439 if h.r.Header.Get("Content-Type") != expectedContentType { 440 log.Error("Content-Type (%q) doesn't match expected: %q", h.r.Header.Get("Content-Type"), expectedContentType) 441 h.w.WriteHeader(http.StatusUnauthorized) 442 return 443 } 444 445 cmd, err := prepareGitCmdWithAllowedService(service, h) 446 if err != nil { 447 log.Error("Failed to prepareGitCmdWithService: %v", err) 448 h.w.WriteHeader(http.StatusUnauthorized) 449 return 450 } 451 452 h.w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-result", service)) 453 454 reqBody := h.r.Body 455 456 // Handle GZIP. 457 if h.r.Header.Get("Content-Encoding") == "gzip" { 458 reqBody, err = gzip.NewReader(reqBody) 459 if err != nil { 460 log.Error("Fail to create gzip reader: %v", err) 461 h.w.WriteHeader(http.StatusInternalServerError) 462 return 463 } 464 } 465 466 // set this for allow pre-receive and post-receive execute 467 h.environ = append(h.environ, "SSH_ORIGINAL_COMMAND="+service) 468 469 if protocol := h.r.Header.Get("Git-Protocol"); protocol != "" && safeGitProtocolHeader.MatchString(protocol) { 470 h.environ = append(h.environ, "GIT_PROTOCOL="+protocol) 471 } 472 473 var stderr bytes.Buffer 474 cmd.AddArguments("--stateless-rpc").AddDynamicArguments(h.dir) 475 cmd.SetDescription(fmt.Sprintf("%s %s %s [repo_path: %s]", git.GitExecutable, service, "--stateless-rpc", h.dir)) 476 if err := cmd.Run(&git.RunOpts{ 477 Dir: h.dir, 478 Env: append(os.Environ(), h.environ...), 479 Stdout: h.w, 480 Stdin: reqBody, 481 Stderr: &stderr, 482 UseContextTimeout: true, 483 }); err != nil { 484 if err.Error() != "signal: killed" { 485 log.Error("Fail to serve RPC(%s) in %s: %v - %s", service, h.dir, err, stderr.String()) 486 } 487 return 488 } 489 } 490 491 // ServiceUploadPack implements Git Smart HTTP protocol 492 func ServiceUploadPack(ctx *context.Context) { 493 h := httpBase(ctx) 494 if h != nil { 495 serviceRPC(h, "upload-pack") 496 } 497 } 498 499 // ServiceReceivePack implements Git Smart HTTP protocol 500 func ServiceReceivePack(ctx *context.Context) { 501 h := httpBase(ctx) 502 if h != nil { 503 serviceRPC(h, "receive-pack") 504 } 505 } 506 507 func getServiceType(r *http.Request) string { 508 serviceType := r.FormValue("service") 509 if !strings.HasPrefix(serviceType, "git-") { 510 return "" 511 } 512 return strings.TrimPrefix(serviceType, "git-") 513 } 514 515 func updateServerInfo(ctx gocontext.Context, dir string) []byte { 516 out, _, err := git.NewCommand(ctx, "update-server-info").RunStdBytes(&git.RunOpts{Dir: dir}) 517 if err != nil { 518 log.Error(fmt.Sprintf("%v - %s", err, string(out))) 519 } 520 return out 521 } 522 523 func packetWrite(str string) []byte { 524 s := strconv.FormatInt(int64(len(str)+4), 16) 525 if len(s)%4 != 0 { 526 s = strings.Repeat("0", 4-len(s)%4) + s 527 } 528 return []byte(s + str) 529 } 530 531 // GetInfoRefs implements Git dumb HTTP 532 func GetInfoRefs(ctx *context.Context) { 533 h := httpBase(ctx) 534 if h == nil { 535 return 536 } 537 h.setHeaderNoCache() 538 service := getServiceType(h.r) 539 cmd, err := prepareGitCmdWithAllowedService(service, h) 540 if err == nil { 541 if protocol := h.r.Header.Get("Git-Protocol"); protocol != "" && safeGitProtocolHeader.MatchString(protocol) { 542 h.environ = append(h.environ, "GIT_PROTOCOL="+protocol) 543 } 544 h.environ = append(os.Environ(), h.environ...) 545 546 refs, _, err := cmd.AddArguments("--stateless-rpc", "--advertise-refs", ".").RunStdBytes(&git.RunOpts{Env: h.environ, Dir: h.dir}) 547 if err != nil { 548 log.Error(fmt.Sprintf("%v - %s", err, string(refs))) 549 } 550 551 h.w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-advertisement", service)) 552 h.w.WriteHeader(http.StatusOK) 553 _, _ = h.w.Write(packetWrite("# service=git-" + service + "\n")) 554 _, _ = h.w.Write([]byte("0000")) 555 _, _ = h.w.Write(refs) 556 } else { 557 updateServerInfo(ctx, h.dir) 558 h.sendFile("text/plain; charset=utf-8", "info/refs") 559 } 560 } 561 562 // GetTextFile implements Git dumb HTTP 563 func GetTextFile(p string) func(*context.Context) { 564 return func(ctx *context.Context) { 565 h := httpBase(ctx) 566 if h != nil { 567 h.setHeaderNoCache() 568 file := ctx.Params("file") 569 if file != "" { 570 h.sendFile("text/plain", "objects/info/"+file) 571 } else { 572 h.sendFile("text/plain", p) 573 } 574 } 575 } 576 } 577 578 // GetInfoPacks implements Git dumb HTTP 579 func GetInfoPacks(ctx *context.Context) { 580 h := httpBase(ctx) 581 if h != nil { 582 h.setHeaderCacheForever() 583 h.sendFile("text/plain; charset=utf-8", "objects/info/packs") 584 } 585 } 586 587 // GetLooseObject implements Git dumb HTTP 588 func GetLooseObject(ctx *context.Context) { 589 h := httpBase(ctx) 590 if h != nil { 591 h.setHeaderCacheForever() 592 h.sendFile("application/x-git-loose-object", fmt.Sprintf("objects/%s/%s", 593 ctx.Params("head"), ctx.Params("hash"))) 594 } 595 } 596 597 // GetPackFile implements Git dumb HTTP 598 func GetPackFile(ctx *context.Context) { 599 h := httpBase(ctx) 600 if h != nil { 601 h.setHeaderCacheForever() 602 h.sendFile("application/x-git-packed-objects", "objects/pack/pack-"+ctx.Params("file")+".pack") 603 } 604 } 605 606 // GetIdxFile implements Git dumb HTTP 607 func GetIdxFile(ctx *context.Context) { 608 h := httpBase(ctx) 609 if h != nil { 610 h.setHeaderCacheForever() 611 h.sendFile("application/x-git-packed-objects-toc", "objects/pack/pack-"+ctx.Params("file")+".idx") 612 } 613 }