mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-30 14:09:42 -05:00
1a68d14cf8
- Follow up of #4819 - When no `ssh` executable is present, disable the UI and backend bits that allow the creation of push mirrors that use SSH authentication. As this feature requires the usage of the `ssh` binary. - Integration test added.
449 lines
12 KiB
Go
449 lines
12 KiB
Go
// Copyright 2020 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package repo
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"code.gitea.io/gitea/models"
|
|
"code.gitea.io/gitea/models/db"
|
|
repo_model "code.gitea.io/gitea/models/repo"
|
|
"code.gitea.io/gitea/models/unit"
|
|
"code.gitea.io/gitea/modules/git"
|
|
"code.gitea.io/gitea/modules/setting"
|
|
api "code.gitea.io/gitea/modules/structs"
|
|
"code.gitea.io/gitea/modules/util"
|
|
"code.gitea.io/gitea/modules/web"
|
|
"code.gitea.io/gitea/routers/api/v1/utils"
|
|
"code.gitea.io/gitea/services/context"
|
|
"code.gitea.io/gitea/services/convert"
|
|
"code.gitea.io/gitea/services/forms"
|
|
"code.gitea.io/gitea/services/migrations"
|
|
mirror_service "code.gitea.io/gitea/services/mirror"
|
|
)
|
|
|
|
// MirrorSync adds a mirrored repository to the sync queue
|
|
func MirrorSync(ctx *context.APIContext) {
|
|
// swagger:operation POST /repos/{owner}/{repo}/mirror-sync repository repoMirrorSync
|
|
// ---
|
|
// summary: Sync a mirrored repository
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - name: owner
|
|
// in: path
|
|
// description: owner of the repo to sync
|
|
// type: string
|
|
// required: true
|
|
// - name: repo
|
|
// in: path
|
|
// description: name of the repo to sync
|
|
// type: string
|
|
// required: true
|
|
// responses:
|
|
// "200":
|
|
// "$ref": "#/responses/empty"
|
|
// "403":
|
|
// "$ref": "#/responses/forbidden"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
// "413":
|
|
// "$ref": "#/responses/quotaExceeded"
|
|
|
|
repo := ctx.Repo.Repository
|
|
|
|
if !ctx.Repo.CanWrite(unit.TypeCode) {
|
|
ctx.Error(http.StatusForbidden, "MirrorSync", "Must have write access")
|
|
}
|
|
|
|
if !setting.Mirror.Enabled {
|
|
ctx.Error(http.StatusBadRequest, "MirrorSync", "Mirror feature is disabled")
|
|
return
|
|
}
|
|
|
|
if _, err := repo_model.GetMirrorByRepoID(ctx, repo.ID); err != nil {
|
|
if errors.Is(err, repo_model.ErrMirrorNotExist) {
|
|
ctx.Error(http.StatusBadRequest, "MirrorSync", "Repository is not a mirror")
|
|
return
|
|
}
|
|
ctx.Error(http.StatusInternalServerError, "MirrorSync", err)
|
|
return
|
|
}
|
|
|
|
mirror_service.AddPullMirrorToQueue(repo.ID)
|
|
|
|
ctx.Status(http.StatusOK)
|
|
}
|
|
|
|
// PushMirrorSync adds all push mirrored repositories to the sync queue
|
|
func PushMirrorSync(ctx *context.APIContext) {
|
|
// swagger:operation POST /repos/{owner}/{repo}/push_mirrors-sync repository repoPushMirrorSync
|
|
// ---
|
|
// summary: Sync all push mirrored repository
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - name: owner
|
|
// in: path
|
|
// description: owner of the repo to sync
|
|
// type: string
|
|
// required: true
|
|
// - name: repo
|
|
// in: path
|
|
// description: name of the repo to sync
|
|
// type: string
|
|
// required: true
|
|
// responses:
|
|
// "200":
|
|
// "$ref": "#/responses/empty"
|
|
// "400":
|
|
// "$ref": "#/responses/error"
|
|
// "403":
|
|
// "$ref": "#/responses/forbidden"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
// "413":
|
|
// "$ref": "#/responses/quotaExceeded"
|
|
|
|
if !setting.Mirror.Enabled {
|
|
ctx.Error(http.StatusBadRequest, "PushMirrorSync", "Mirror feature is disabled")
|
|
return
|
|
}
|
|
// Get All push mirrors of a specific repo
|
|
pushMirrors, _, err := repo_model.GetPushMirrorsByRepoID(ctx, ctx.Repo.Repository.ID, db.ListOptions{})
|
|
if err != nil {
|
|
ctx.Error(http.StatusNotFound, "PushMirrorSync", err)
|
|
return
|
|
}
|
|
for _, mirror := range pushMirrors {
|
|
ok := mirror_service.SyncPushMirror(ctx, mirror.ID)
|
|
if !ok {
|
|
ctx.Error(http.StatusInternalServerError, "PushMirrorSync", "error occurred when syncing push mirror "+mirror.RemoteName)
|
|
return
|
|
}
|
|
}
|
|
|
|
ctx.Status(http.StatusOK)
|
|
}
|
|
|
|
// ListPushMirrors get list of push mirrors of a repository
|
|
func ListPushMirrors(ctx *context.APIContext) {
|
|
// swagger:operation GET /repos/{owner}/{repo}/push_mirrors repository repoListPushMirrors
|
|
// ---
|
|
// summary: Get all push mirrors of the repository
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - name: owner
|
|
// in: path
|
|
// description: owner of the repo
|
|
// type: string
|
|
// required: true
|
|
// - name: repo
|
|
// in: path
|
|
// description: name of the repo
|
|
// type: string
|
|
// required: true
|
|
// - name: page
|
|
// in: query
|
|
// description: page number of results to return (1-based)
|
|
// type: integer
|
|
// - name: limit
|
|
// in: query
|
|
// description: page size of results
|
|
// type: integer
|
|
// responses:
|
|
// "200":
|
|
// "$ref": "#/responses/PushMirrorList"
|
|
// "400":
|
|
// "$ref": "#/responses/error"
|
|
// "403":
|
|
// "$ref": "#/responses/forbidden"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
|
|
if !setting.Mirror.Enabled {
|
|
ctx.Error(http.StatusBadRequest, "GetPushMirrorsByRepoID", "Mirror feature is disabled")
|
|
return
|
|
}
|
|
|
|
repo := ctx.Repo.Repository
|
|
// Get all push mirrors for the specified repository.
|
|
pushMirrors, count, err := repo_model.GetPushMirrorsByRepoID(ctx, repo.ID, utils.GetListOptions(ctx))
|
|
if err != nil {
|
|
ctx.Error(http.StatusNotFound, "GetPushMirrorsByRepoID", err)
|
|
return
|
|
}
|
|
|
|
responsePushMirrors := make([]*api.PushMirror, 0, len(pushMirrors))
|
|
for _, mirror := range pushMirrors {
|
|
m, err := convert.ToPushMirror(ctx, mirror)
|
|
if err == nil {
|
|
responsePushMirrors = append(responsePushMirrors, m)
|
|
}
|
|
}
|
|
ctx.SetLinkHeader(len(responsePushMirrors), utils.GetListOptions(ctx).PageSize)
|
|
ctx.SetTotalCountHeader(count)
|
|
ctx.JSON(http.StatusOK, responsePushMirrors)
|
|
}
|
|
|
|
// GetPushMirrorByName get push mirror of a repository by name
|
|
func GetPushMirrorByName(ctx *context.APIContext) {
|
|
// swagger:operation GET /repos/{owner}/{repo}/push_mirrors/{name} repository repoGetPushMirrorByRemoteName
|
|
// ---
|
|
// summary: Get push mirror of the repository by remoteName
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - name: owner
|
|
// in: path
|
|
// description: owner of the repo
|
|
// type: string
|
|
// required: true
|
|
// - name: repo
|
|
// in: path
|
|
// description: name of the repo
|
|
// type: string
|
|
// required: true
|
|
// - name: name
|
|
// in: path
|
|
// description: remote name of push mirror
|
|
// type: string
|
|
// required: true
|
|
// responses:
|
|
// "200":
|
|
// "$ref": "#/responses/PushMirror"
|
|
// "400":
|
|
// "$ref": "#/responses/error"
|
|
// "403":
|
|
// "$ref": "#/responses/forbidden"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
|
|
if !setting.Mirror.Enabled {
|
|
ctx.Error(http.StatusBadRequest, "GetPushMirrorByRemoteName", "Mirror feature is disabled")
|
|
return
|
|
}
|
|
|
|
mirrorName := ctx.Params(":name")
|
|
// Get push mirror of a specific repo by remoteName
|
|
pushMirror, exist, err := db.Get[repo_model.PushMirror](ctx, repo_model.PushMirrorOptions{
|
|
RepoID: ctx.Repo.Repository.ID,
|
|
RemoteName: mirrorName,
|
|
}.ToConds())
|
|
if err != nil {
|
|
ctx.Error(http.StatusInternalServerError, "GetPushMirrors", err)
|
|
return
|
|
} else if !exist {
|
|
ctx.Error(http.StatusNotFound, "GetPushMirrors", nil)
|
|
return
|
|
}
|
|
|
|
m, err := convert.ToPushMirror(ctx, pushMirror)
|
|
if err != nil {
|
|
ctx.ServerError("GetPushMirrorByRemoteName", err)
|
|
return
|
|
}
|
|
ctx.JSON(http.StatusOK, m)
|
|
}
|
|
|
|
// AddPushMirror adds a push mirror to a repository
|
|
func AddPushMirror(ctx *context.APIContext) {
|
|
// swagger:operation POST /repos/{owner}/{repo}/push_mirrors repository repoAddPushMirror
|
|
// ---
|
|
// summary: add a push mirror to the repository
|
|
// consumes:
|
|
// - application/json
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - name: owner
|
|
// in: path
|
|
// description: owner of the repo
|
|
// type: string
|
|
// required: true
|
|
// - name: repo
|
|
// in: path
|
|
// description: name of the repo
|
|
// type: string
|
|
// required: true
|
|
// - name: body
|
|
// in: body
|
|
// schema:
|
|
// "$ref": "#/definitions/CreatePushMirrorOption"
|
|
// responses:
|
|
// "200":
|
|
// "$ref": "#/responses/PushMirror"
|
|
// "403":
|
|
// "$ref": "#/responses/forbidden"
|
|
// "400":
|
|
// "$ref": "#/responses/error"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
// "413":
|
|
// "$ref": "#/responses/quotaExceeded"
|
|
|
|
if !setting.Mirror.Enabled {
|
|
ctx.Error(http.StatusBadRequest, "AddPushMirror", "Mirror feature is disabled")
|
|
return
|
|
}
|
|
|
|
pushMirror := web.GetForm(ctx).(*api.CreatePushMirrorOption)
|
|
CreatePushMirror(ctx, pushMirror)
|
|
}
|
|
|
|
// DeletePushMirrorByRemoteName deletes a push mirror from a repository by remoteName
|
|
func DeletePushMirrorByRemoteName(ctx *context.APIContext) {
|
|
// swagger:operation DELETE /repos/{owner}/{repo}/push_mirrors/{name} repository repoDeletePushMirror
|
|
// ---
|
|
// summary: deletes a push mirror from a repository by remoteName
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - name: owner
|
|
// in: path
|
|
// description: owner of the repo
|
|
// type: string
|
|
// required: true
|
|
// - name: repo
|
|
// in: path
|
|
// description: name of the repo
|
|
// type: string
|
|
// required: true
|
|
// - name: name
|
|
// in: path
|
|
// description: remote name of the pushMirror
|
|
// type: string
|
|
// required: true
|
|
// responses:
|
|
// "204":
|
|
// "$ref": "#/responses/empty"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
// "400":
|
|
// "$ref": "#/responses/error"
|
|
|
|
if !setting.Mirror.Enabled {
|
|
ctx.Error(http.StatusBadRequest, "DeletePushMirrorByName", "Mirror feature is disabled")
|
|
return
|
|
}
|
|
|
|
remoteName := ctx.Params(":name")
|
|
// Delete push mirror on repo by name.
|
|
err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{RepoID: ctx.Repo.Repository.ID, RemoteName: remoteName})
|
|
if err != nil {
|
|
ctx.Error(http.StatusNotFound, "DeletePushMirrors", err)
|
|
return
|
|
}
|
|
ctx.Status(http.StatusNoContent)
|
|
}
|
|
|
|
func CreatePushMirror(ctx *context.APIContext, mirrorOption *api.CreatePushMirrorOption) {
|
|
repo := ctx.Repo.Repository
|
|
|
|
interval, err := time.ParseDuration(mirrorOption.Interval)
|
|
if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) {
|
|
ctx.Error(http.StatusBadRequest, "CreatePushMirror", err)
|
|
return
|
|
}
|
|
|
|
if mirrorOption.UseSSH && !git.HasSSHExecutable {
|
|
ctx.Error(http.StatusBadRequest, "CreatePushMirror", "SSH authentication not available.")
|
|
return
|
|
}
|
|
|
|
if mirrorOption.UseSSH && (mirrorOption.RemoteUsername != "" || mirrorOption.RemotePassword != "") {
|
|
ctx.Error(http.StatusBadRequest, "CreatePushMirror", "'use_ssh' is mutually exclusive with 'remote_username' and 'remote_passoword'")
|
|
return
|
|
}
|
|
|
|
address, err := forms.ParseRemoteAddr(mirrorOption.RemoteAddress, mirrorOption.RemoteUsername, mirrorOption.RemotePassword)
|
|
if err == nil {
|
|
err = migrations.IsMigrateURLAllowed(address, ctx.ContextUser)
|
|
}
|
|
if err != nil {
|
|
HandleRemoteAddressError(ctx, err)
|
|
return
|
|
}
|
|
|
|
remoteSuffix, err := util.CryptoRandomString(10)
|
|
if err != nil {
|
|
ctx.ServerError("CryptoRandomString", err)
|
|
return
|
|
}
|
|
|
|
remoteAddress, err := util.SanitizeURL(address)
|
|
if err != nil {
|
|
ctx.ServerError("SanitizeURL", err)
|
|
return
|
|
}
|
|
|
|
pushMirror := &repo_model.PushMirror{
|
|
RepoID: repo.ID,
|
|
Repo: repo,
|
|
RemoteName: fmt.Sprintf("remote_mirror_%s", remoteSuffix),
|
|
Interval: interval,
|
|
SyncOnCommit: mirrorOption.SyncOnCommit,
|
|
RemoteAddress: remoteAddress,
|
|
}
|
|
|
|
var plainPrivateKey []byte
|
|
if mirrorOption.UseSSH {
|
|
publicKey, privateKey, err := util.GenerateSSHKeypair()
|
|
if err != nil {
|
|
ctx.ServerError("GenerateSSHKeypair", err)
|
|
return
|
|
}
|
|
plainPrivateKey = privateKey
|
|
pushMirror.PublicKey = string(publicKey)
|
|
}
|
|
|
|
if err = db.Insert(ctx, pushMirror); err != nil {
|
|
ctx.ServerError("InsertPushMirror", err)
|
|
return
|
|
}
|
|
|
|
if mirrorOption.UseSSH {
|
|
if err = pushMirror.SetPrivatekey(ctx, plainPrivateKey); err != nil {
|
|
ctx.ServerError("SetPrivatekey", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
// if the registration of the push mirrorOption fails remove it from the database
|
|
if err = mirror_service.AddPushMirrorRemote(ctx, pushMirror, address); err != nil {
|
|
if err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{ID: pushMirror.ID, RepoID: pushMirror.RepoID}); err != nil {
|
|
ctx.ServerError("DeletePushMirrors", err)
|
|
return
|
|
}
|
|
ctx.ServerError("AddPushMirrorRemote", err)
|
|
return
|
|
}
|
|
m, err := convert.ToPushMirror(ctx, pushMirror)
|
|
if err != nil {
|
|
ctx.ServerError("ToPushMirror", err)
|
|
return
|
|
}
|
|
ctx.JSON(http.StatusOK, m)
|
|
}
|
|
|
|
func HandleRemoteAddressError(ctx *context.APIContext, err error) {
|
|
if models.IsErrInvalidCloneAddr(err) {
|
|
addrErr := err.(*models.ErrInvalidCloneAddr)
|
|
switch {
|
|
case addrErr.IsProtocolInvalid:
|
|
ctx.Error(http.StatusBadRequest, "CreatePushMirror", "Invalid mirror protocol")
|
|
case addrErr.IsURLError:
|
|
ctx.Error(http.StatusBadRequest, "CreatePushMirror", "Invalid Url ")
|
|
case addrErr.IsPermissionDenied:
|
|
ctx.Error(http.StatusUnauthorized, "CreatePushMirror", "Permission denied")
|
|
default:
|
|
ctx.Error(http.StatusBadRequest, "CreatePushMirror", "Unknown error")
|
|
}
|
|
return
|
|
}
|
|
}
|