mirror of https://github.com/harness/drone.git
255 lines
7.3 KiB
Go
255 lines
7.3 KiB
Go
// Copyright 2023 Harness, Inc.
|
|
//
|
|
// 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 server
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/gzip"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
gitnesshttp "github.com/harness/gitness/http"
|
|
|
|
"code.gitea.io/gitea/modules/git"
|
|
"github.com/go-chi/chi"
|
|
"github.com/go-chi/chi/middleware"
|
|
"github.com/rs/zerolog/hlog"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
const (
|
|
PathParamRepoUID = "repoUID"
|
|
)
|
|
|
|
var (
|
|
safeGitProtocolHeader = regexp.MustCompile(`^[0-9a-zA-Z]+=[0-9a-zA-Z]+(:[0-9a-zA-Z]+=[0-9a-zA-Z]+)*$`)
|
|
)
|
|
|
|
// HTTPServer exposes the gitrpc rest api.
|
|
type HTTPServer struct {
|
|
*gitnesshttp.Server
|
|
}
|
|
|
|
func NewHTTPServer(config Config) (*HTTPServer, error) {
|
|
if err := config.Validate(); err != nil {
|
|
return nil, fmt.Errorf("configuration is invalid: %w", err)
|
|
}
|
|
|
|
reposRoot := filepath.Join(config.GitRoot, repoSubdirName)
|
|
|
|
return &HTTPServer{
|
|
gitnesshttp.NewServer(
|
|
gitnesshttp.Config{
|
|
Port: config.HTTP.Port,
|
|
},
|
|
handleHTTP(reposRoot),
|
|
),
|
|
}, nil
|
|
}
|
|
|
|
func handleHTTP(reposRoot string) http.Handler {
|
|
r := chi.NewRouter()
|
|
|
|
// Apply common api middleware.
|
|
r.Use(middleware.NoCache)
|
|
r.Use(middleware.Recoverer)
|
|
|
|
// configure logging middleware.
|
|
log := log.Logger.With().Logger()
|
|
r.Use(hlog.NewHandler(log))
|
|
r.Use(hlog.URLHandler("http.url"))
|
|
r.Use(hlog.MethodHandler("http.method"))
|
|
r.Use(HLogRequestIDHandler())
|
|
r.Use(HLogAccessLogHandler())
|
|
|
|
r.Route(fmt.Sprintf("/{%s}", PathParamRepoUID), func(r chi.Router) {
|
|
r.Get("/info/refs", handleHTTPInfoRefs(reposRoot))
|
|
r.Handle("/git-upload-pack", handleHTTPUploadPack(reposRoot))
|
|
|
|
// push is not supported
|
|
r.Post("/git-receive-pack", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusNotImplemented)
|
|
_, _ = w.Write([]byte("receive pack is not supported by this endpoint"))
|
|
})
|
|
})
|
|
|
|
return r
|
|
}
|
|
|
|
func handleHTTPInfoRefs(reposRoot string) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
// Clients MUST NOT reuse or revalidate a cached response.
|
|
// Servers MUST include sufficient Cache-Control headers to prevent caching of the response.
|
|
// https://git-scm.com/docs/http-protocol
|
|
setHeaderNoCache(w)
|
|
|
|
repoUID := chi.URLParam(r, PathParamRepoUID)
|
|
repoPath := getFullPathForRepo(reposRoot, repoUID)
|
|
gitProtocol := r.Header.Get("Git-Protocol")
|
|
service := getServiceType(r)
|
|
|
|
log.Ctx(ctx).Trace().Msgf(
|
|
"handleHTTPInfoRefs for git service: '%s', protocol: '%s', path: '%s'",
|
|
service,
|
|
gitProtocol,
|
|
repoPath,
|
|
)
|
|
|
|
w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-advertisement", service))
|
|
|
|
// NOTE: Don't include os.Environ() as we don't have control over it - define everything explicitly
|
|
environ := []string{}
|
|
if gitProtocol != "" {
|
|
environ = append(environ, "GIT_PROTOCOL="+gitProtocol)
|
|
}
|
|
|
|
stdOut := &bytes.Buffer{}
|
|
if err := git.NewCommand(ctx, service, "--stateless-rpc", "--advertise-refs", ".").
|
|
Run(&git.RunOpts{
|
|
Env: environ,
|
|
Dir: repoPath,
|
|
Stdout: stdOut,
|
|
}); err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
log.Ctx(ctx).Error().Err(err).Msgf("failed running git command")
|
|
|
|
return
|
|
}
|
|
if _, err := w.Write(packetWrite("# service=git-" + service + "\n")); err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
log.Ctx(ctx).Error().Err(err).Msgf("failed writing packet line")
|
|
|
|
return
|
|
}
|
|
|
|
if _, err := w.Write([]byte("0000")); err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
log.Ctx(ctx).Error().Err(err).Msgf("failed writing end of response")
|
|
|
|
return
|
|
}
|
|
|
|
if _, err := io.Copy(w, stdOut); err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
log.Ctx(ctx).Warn().Err(err).Msgf("failed copying response body")
|
|
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func handleHTTPUploadPack(reposRoot string) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
const service = "upload-pack"
|
|
repoUID := chi.URLParam(r, PathParamRepoUID)
|
|
repoPath := getFullPathForRepo(reposRoot, repoUID)
|
|
gitProtocol := r.Header.Get("Git-Protocol")
|
|
|
|
log.Ctx(ctx).Trace().Msgf(
|
|
"handleHTTPUploadPack for git service: '%s', protocol: '%s', path: '%s'",
|
|
service,
|
|
gitProtocol,
|
|
repoPath,
|
|
)
|
|
|
|
w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-result", service))
|
|
|
|
var err error
|
|
reqBody := r.Body
|
|
|
|
// Handle GZIP.
|
|
if r.Header.Get("Content-Encoding") == "gzip" {
|
|
reqBody, err = gzip.NewReader(reqBody)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
log.Ctx(ctx).Error().Err(err).Msgf("failed gziping response body")
|
|
return
|
|
}
|
|
}
|
|
|
|
// NOTE: Don't include os.Environ() as we don't have control over it - define everything explicitly
|
|
environ := []string{}
|
|
// set this for allow pre-receive and post-receive execute
|
|
environ = append(environ, "SSH_ORIGINAL_COMMAND="+service)
|
|
if gitProtocol != "" && safeGitProtocolHeader.MatchString(gitProtocol) {
|
|
environ = append(environ, "GIT_PROTOCOL="+gitProtocol)
|
|
}
|
|
|
|
var (
|
|
stderr bytes.Buffer
|
|
)
|
|
cmd := git.NewCommand(ctx, service, "--stateless-rpc", repoPath)
|
|
cmd.SetDescription(fmt.Sprintf("%s %s %s [repo_path: %s]", git.GitExecutable, service, "--stateless-rpc", repoPath))
|
|
err = cmd.Run(&git.RunOpts{
|
|
Dir: repoPath,
|
|
Env: environ,
|
|
Stdout: w,
|
|
Stdin: reqBody,
|
|
Stderr: &stderr,
|
|
UseContextTimeout: true,
|
|
})
|
|
if err != nil {
|
|
log.Ctx(ctx).Error().Err(err).Msgf("Failed to serve RPC(%s) in %s: %v - %s", service, repoPath, err, stderr.String())
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}
|
|
}
|
|
}
|
|
|
|
func setHeaderNoCache(w http.ResponseWriter) {
|
|
w.Header().Set("Expires", "Fri, 01 Jan 1980 00:00:00 GMT")
|
|
w.Header().Set("Pragma", "no-cache")
|
|
w.Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate")
|
|
}
|
|
|
|
func getServiceType(r *http.Request) string {
|
|
serviceType := r.URL.Query().Get("service")
|
|
if !strings.HasPrefix(serviceType, "git-") {
|
|
return ""
|
|
}
|
|
return strings.Replace(serviceType, "git-", "", 1)
|
|
}
|
|
|
|
// getFullPathForRepo returns the full path of a repo given the root dir of repos and the uid of the repo.
|
|
// NOTE: Split repos into subfolders using their prefix to distribute repos across a set of folders.
|
|
// TODO: Use common function between grpc and git server
|
|
func getFullPathForRepo(reposRoot, uid string) string {
|
|
// ASSUMPTION: repoUID is of lenth at least 4 - otherwise we have trouble either way.
|
|
return filepath.Join(
|
|
reposRoot, // root folder
|
|
uid[0:2], // first subfolder
|
|
uid[2:4], // second subfolder
|
|
fmt.Sprintf("%s.%s", uid[4:], "git"), // remainder with .git
|
|
)
|
|
}
|
|
|
|
func packetWrite(str string) []byte {
|
|
s := strconv.FormatInt(int64(len(str)+4), 16)
|
|
if len(s)%4 != 0 {
|
|
s = strings.Repeat("0", 4-len(s)%4) + s
|
|
}
|
|
return []byte(s + str)
|
|
}
|