279 lines
8.0 KiB
Go
279 lines
8.0 KiB
Go
// Copyright 2019 Sorint.lab
|
|
//
|
|
// 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 githandler
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/gzip"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"agola.io/agola/internal/errors"
|
|
"agola.io/agola/internal/util"
|
|
|
|
"github.com/rs/zerolog"
|
|
)
|
|
|
|
var (
|
|
InfoRefsRegExp = regexp.MustCompile(`/(.+\.git)/info/refs$`)
|
|
UploadPackRegExp = regexp.MustCompile(`/(.+\.git)/git-upload-pack$`)
|
|
ReceivePackRegExp = regexp.MustCompile(`/(.+\.git)/git-receive-pack$`)
|
|
|
|
FetchFileRegExp = regexp.MustCompile(`/(.+\.git)/raw/(.+?)/(.+)`)
|
|
)
|
|
|
|
type RequestType int
|
|
|
|
const (
|
|
RequestTypeInfoRefs RequestType = iota
|
|
RequestTypeUploadPack
|
|
RequestTypeReceivePack
|
|
)
|
|
|
|
type FetchFileData struct {
|
|
RepoPath string
|
|
Ref string
|
|
Path string
|
|
}
|
|
|
|
func ParseFetchFilePath(path string) (*FetchFileData, error) {
|
|
matches := FetchFileRegExp.FindStringSubmatch(path)
|
|
if len(matches) != 4 {
|
|
return nil, errors.New("cannot get fetch file data from url")
|
|
}
|
|
return &FetchFileData{
|
|
RepoPath: matches[1],
|
|
Ref: matches[2],
|
|
Path: matches[3],
|
|
}, nil
|
|
}
|
|
|
|
func MatchPath(path string) (string, RequestType, error) {
|
|
var matchedRegExp *regexp.Regexp
|
|
var reqType RequestType
|
|
for i, regExp := range []*regexp.Regexp{InfoRefsRegExp, UploadPackRegExp, ReceivePackRegExp} {
|
|
if regExp.MatchString(path) {
|
|
matchedRegExp = regExp
|
|
reqType = RequestType(i)
|
|
}
|
|
}
|
|
if matchedRegExp == nil {
|
|
return "", 0, errors.New("wrong request path")
|
|
}
|
|
|
|
matches := matchedRegExp.FindStringSubmatch(path)
|
|
if len(matches) != 2 {
|
|
return "", 0, errors.New("cannot get repository path from url")
|
|
}
|
|
return matches[1], reqType, nil
|
|
}
|
|
|
|
func gitServiceName(r *http.Request) (string, error) {
|
|
service := r.URL.Query().Get("service")
|
|
if !strings.HasPrefix(service, "git-") {
|
|
return "", errors.Errorf("wrong git service %q", service)
|
|
}
|
|
return strings.TrimPrefix(service, "git-"), nil
|
|
}
|
|
|
|
func writePacketLine(w io.Writer, line string) {
|
|
fmt.Fprintf(w, "%.4x%s\n", len(line)+5, line)
|
|
}
|
|
|
|
func writeFlushPacket(w io.Writer) {
|
|
fmt.Fprintf(w, "0000")
|
|
}
|
|
|
|
func InfoRefsResponse(ctx context.Context, repoPath, serviceName string) ([]byte, error) {
|
|
buf := &bytes.Buffer{}
|
|
|
|
writePacketLine(buf, "# service=git-"+serviceName)
|
|
writeFlushPacket(buf)
|
|
|
|
git := &util.Git{}
|
|
out, err := git.Output(ctx, nil, serviceName, "--stateless-rpc", "--advertise-refs", repoPath)
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
buf.Write(out)
|
|
|
|
return buf.Bytes(), errors.WithStack(err)
|
|
}
|
|
|
|
func gitService(ctx context.Context, w io.Writer, r io.Reader, repoPath, serviceName string) error {
|
|
git := &util.Git{GitDir: repoPath}
|
|
return errors.WithStack(git.Pipe(ctx, w, r, serviceName, "--stateless-rpc", repoPath))
|
|
}
|
|
|
|
func gitFetchFile(ctx context.Context, w io.Writer, r io.Reader, repoPath, ref, path string) error {
|
|
git := &util.Git{GitDir: repoPath}
|
|
return errors.WithStack(git.Pipe(ctx, w, r, "show", fmt.Sprintf("%s:%s", ref, path)))
|
|
}
|
|
|
|
var ErrWrongRepoPath = errors.New("wrong repository path")
|
|
|
|
// RepoAbsPathFunc is a user defined functions that, given the repo path
|
|
// provided in the url request, will return the file system absolute repo path
|
|
// and if it exists.
|
|
// This function should also do path validation and return ErrWrongRepoPath if
|
|
// path validation failed.
|
|
type RepoAbsPathFunc func(reposDir, path string) (absPath string, exists bool, err error)
|
|
|
|
type RepoPostCreateFunc func(repoPath, repoAbsPath string) error
|
|
|
|
type GitSmartHandler struct {
|
|
log zerolog.Logger
|
|
reposDir string
|
|
createRepo bool
|
|
repoAbsPathFunc RepoAbsPathFunc
|
|
repoPostCreateFunc RepoPostCreateFunc
|
|
}
|
|
|
|
func NewGitSmartHandler(log zerolog.Logger, reposDir string, createRepo bool, repoAbsPathFunc RepoAbsPathFunc, repoPostCreateFunc RepoPostCreateFunc) *GitSmartHandler {
|
|
return &GitSmartHandler{
|
|
log: log,
|
|
reposDir: reposDir,
|
|
createRepo: createRepo,
|
|
repoAbsPathFunc: repoAbsPathFunc,
|
|
repoPostCreateFunc: repoPostCreateFunc,
|
|
}
|
|
}
|
|
|
|
func (h *GitSmartHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
repoPath, reqType, err := MatchPath(r.URL.Path)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
repoAbsPath, exists, err := h.repoAbsPathFunc(h.reposDir, repoPath)
|
|
if err != nil {
|
|
if errors.Is(err, ErrWrongRepoPath) {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
git := &util.Git{GitDir: repoAbsPath}
|
|
|
|
body := r.Body
|
|
if r.Header.Get("Content-Encoding") == "gzip" {
|
|
body, err = gzip.NewReader(r.Body)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
}
|
|
}
|
|
|
|
switch reqType {
|
|
case RequestTypeInfoRefs:
|
|
if h.createRepo && !exists {
|
|
if output, err := git.Output(ctx, nil, "init", "--bare", repoAbsPath); err != nil {
|
|
h.log.Err(err).Msgf("git error, output: %s", output)
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if h.repoPostCreateFunc != nil {
|
|
if err := h.repoPostCreateFunc(repoPath, repoAbsPath); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
serviceName, err := gitServiceName(r)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
res, err := InfoRefsResponse(ctx, repoAbsPath, serviceName)
|
|
if err != nil {
|
|
// we cannot return any http error since the http header has already been written
|
|
h.log.Err(err).Msgf("git command error")
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/x-git-"+serviceName+"-advertisement")
|
|
_, _ = w.Write(res)
|
|
|
|
case RequestTypeUploadPack:
|
|
w.Header().Set("Content-Type", "application/x-git-upload-pack-result")
|
|
|
|
if err := gitService(ctx, w, body, repoAbsPath, "upload-pack"); err != nil {
|
|
// we cannot return any http error since the http header has already been written
|
|
h.log.Err(err).Msgf("git command error")
|
|
}
|
|
case RequestTypeReceivePack:
|
|
w.Header().Set("Content-Type", "application/x-git-receive-pack-result")
|
|
|
|
if err := gitService(ctx, w, body, repoAbsPath, "receive-pack"); err != nil {
|
|
// we cannot return any http error since the http header has already been written
|
|
h.log.Err(err).Msgf("git command error")
|
|
}
|
|
}
|
|
}
|
|
|
|
type FetchFileHandler struct {
|
|
log zerolog.Logger
|
|
reposDir string
|
|
repoAbsPathFunc RepoAbsPathFunc
|
|
}
|
|
|
|
func NewFetchFileHandler(log zerolog.Logger, reposDir string, repoAbsPathFunc RepoAbsPathFunc) *FetchFileHandler {
|
|
return &FetchFileHandler{
|
|
log: log,
|
|
reposDir: reposDir,
|
|
repoAbsPathFunc: repoAbsPathFunc,
|
|
}
|
|
}
|
|
|
|
func (h *FetchFileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
fetchData, err := ParseFetchFilePath(r.URL.Path)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
repoAbsPath, _, err := h.repoAbsPathFunc(h.reposDir, fetchData.RepoPath)
|
|
if err != nil {
|
|
if errors.Is(err, ErrWrongRepoPath) {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
if err := gitFetchFile(ctx, w, r.Body, repoAbsPath, fetchData.Ref, fetchData.Path); err != nil {
|
|
h.log.Err(err).Msgf("git command error")
|
|
|
|
// since we already answered with a 200 we cannot return another error code
|
|
// So abort the connection and the client will detect the missing ending chunk
|
|
// and consider this an error
|
|
//
|
|
// this is the way to force close a request without logging the panic
|
|
panic(http.ErrAbortHandler)
|
|
}
|
|
}
|