Init gist with regular urls via git CLI (http) (#501)
This commit is contained in:
@@ -15,17 +15,13 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/thomiceli/opengist/internal/auth/ldap"
|
||||
"github.com/thomiceli/opengist/internal/auth/password"
|
||||
"github.com/thomiceli/opengist/internal/web/context"
|
||||
"github.com/thomiceli/opengist/internal/web/handlers"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/thomiceli/opengist/internal/auth"
|
||||
"github.com/thomiceli/opengist/internal/db"
|
||||
"github.com/thomiceli/opengist/internal/git"
|
||||
"gorm.io/gorm"
|
||||
"github.com/thomiceli/opengist/internal/web/context"
|
||||
"github.com/thomiceli/opengist/internal/web/handlers"
|
||||
)
|
||||
|
||||
var routes = []struct {
|
||||
@@ -47,165 +43,211 @@ var routes = []struct {
|
||||
}
|
||||
|
||||
func GitHttp(ctx *context.Context) error {
|
||||
route := findMatchingRoute(ctx)
|
||||
if route == nil {
|
||||
return ctx.NotFound("Gist not found") // regular 404 for non-git routes
|
||||
}
|
||||
gist := ctx.GetData("gist").(*db.Gist)
|
||||
gistExists := gist.ID != 0
|
||||
|
||||
isInitRoute := strings.HasPrefix(ctx.Request().URL.Path, "/init/info/refs")
|
||||
isInitRouteReceive := strings.HasPrefix(ctx.Request().URL.Path, "/init/git-receive-pack")
|
||||
isInfoRefs := strings.HasSuffix(route.gitUrl, "/info/refs$")
|
||||
isPull := ctx.QueryParam("service") == "git-upload-pack" ||
|
||||
strings.HasSuffix(ctx.Request().URL.Path, "git-upload-pack") && !isInfoRefs
|
||||
isPush := ctx.QueryParam("service") == "git-receive-pack" ||
|
||||
strings.HasSuffix(ctx.Request().URL.Path, "git-receive-pack") && !isInfoRefs
|
||||
|
||||
repositoryPath := git.RepositoryPath(gist.User.Username, gist.Uuid)
|
||||
ctx.SetData("repositoryPath", repositoryPath)
|
||||
|
||||
allow, err := auth.ShouldAllowUnauthenticatedGistAccess(handlers.ContextAuthInfo{Context: ctx}, true)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Cannot check if unauthenticated access is allowed")
|
||||
}
|
||||
|
||||
// No need to authenticate if the user wants
|
||||
// to clone/pull ; a non-private gist ; that exists ; where unauthenticated access is allowed in the instance
|
||||
if isPull && gist.Private != db.PrivateVisibility && gistExists && allow {
|
||||
return route.handler(ctx)
|
||||
}
|
||||
|
||||
// Else we need to authenticate the user, that include other cases:
|
||||
// - user wants to push the gist
|
||||
// - user wants to clone/pull a private gist
|
||||
// - user wants to clone/pull a non-private gist but unauthenticated access is not allowed
|
||||
// - gist is not found ; has no right to clone/pull (obfuscation)
|
||||
// - admin setting to require login is set to true
|
||||
authUsername, authPassword, err := parseAuthHeader(ctx)
|
||||
if err != nil {
|
||||
return basicAuth(ctx)
|
||||
}
|
||||
|
||||
// if the user wants to create a gist via the /init route
|
||||
if isInitRoute || isInitRouteReceive {
|
||||
var user *db.User
|
||||
|
||||
// check if the user has a valid account on opengist to push a gist
|
||||
user, err = auth.TryAuthentication(authUsername, authPassword)
|
||||
if err != nil {
|
||||
var authErr auth.AuthError
|
||||
if errors.As(err, &authErr) {
|
||||
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
|
||||
return ctx.PlainText(401, "Invalid credentials")
|
||||
}
|
||||
return ctx.ErrorRes(500, "Authentication system error", nil)
|
||||
}
|
||||
|
||||
if isInitRoute {
|
||||
gist, err = createGist(user, "")
|
||||
if err != nil {
|
||||
return ctx.ErrorRes(500, "Cannot create gist", err)
|
||||
}
|
||||
|
||||
err = db.AddInitGistToQueue(gist.ID, user.ID)
|
||||
if err != nil {
|
||||
return ctx.ErrorRes(500, "Cannot add inited gist to the queue", err)
|
||||
}
|
||||
ctx.SetData("gist", gist)
|
||||
return route.handler(ctx)
|
||||
} else {
|
||||
gist, err = db.GetInitGistInQueueForUser(user.ID)
|
||||
if err != nil {
|
||||
return ctx.ErrorRes(500, "Cannot retrieve inited gist from the queue", err)
|
||||
}
|
||||
|
||||
ctx.SetData("gist", gist)
|
||||
ctx.SetData("repositoryPath", git.RepositoryPath(gist.User.Username, gist.Uuid))
|
||||
return route.handler(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// if clone/pull
|
||||
// check if the gist exists and if the credentials are valid
|
||||
if isPull {
|
||||
log.Debug().Msg("Detected git pull operation")
|
||||
if !gistExists {
|
||||
log.Debug().Str("authUsername", authUsername).Msg("Pulling unknown gist")
|
||||
return ctx.PlainText(404, "Check your credentials or make sure you have access to the Gist")
|
||||
}
|
||||
|
||||
var userToCheckPermissions string
|
||||
// if the user is trying to clone/pull a non-private gist while unauthenticated access is not allowed,
|
||||
// check if the user has a valid account
|
||||
if gist.Private != db.PrivateVisibility {
|
||||
log.Debug().Str("authUsername", authUsername).Msg("Pulling non-private gist with authenticated access")
|
||||
userToCheckPermissions = authUsername
|
||||
} else { // else just check the password against the gist owner
|
||||
log.Debug().Str("authUsername", authUsername).Str("gistOwner", gist.User.Username).Msg("Pulling private gist")
|
||||
userToCheckPermissions = gist.User.Username
|
||||
}
|
||||
|
||||
if _, err = auth.TryAuthentication(userToCheckPermissions, authPassword); err != nil {
|
||||
var authErr auth.AuthError
|
||||
if errors.As(err, &authErr) {
|
||||
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
|
||||
return ctx.PlainText(404, "Check your credentials or make sure you have access to the Gist")
|
||||
}
|
||||
return ctx.ErrorRes(500, "Authentication system error", nil)
|
||||
}
|
||||
log.Debug().Str("authUsername", authUsername).Msg("Pulling gist")
|
||||
|
||||
return route.handler(ctx)
|
||||
}
|
||||
|
||||
if isPush {
|
||||
log.Debug().Msg("Detected git push operation")
|
||||
// if gist exists, check if the credentials are valid and if the user is the gist owner
|
||||
if gistExists {
|
||||
log.Debug().Str("authUsername", authUsername).Str("gistOwner", gist.User.Username).Msg("Pushing to existing gist")
|
||||
if _, err = auth.TryAuthentication(gist.User.Username, authPassword); err != nil {
|
||||
var authErr auth.AuthError
|
||||
if errors.As(err, &authErr) {
|
||||
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
|
||||
return ctx.PlainText(404, "Check your credentials or make sure you have access to the Gist")
|
||||
}
|
||||
return ctx.ErrorRes(500, "Authentication system error", nil)
|
||||
}
|
||||
log.Debug().Str("authUsername", authUsername).Msg("Pushing gist")
|
||||
|
||||
return route.handler(ctx)
|
||||
} else { // if the gist does not exist, check if the user has a valid account on opengist to push a gist and create it
|
||||
log.Debug().Str("authUsername", authUsername).Msg("Creating new gist by pushing")
|
||||
var user *db.User
|
||||
if user, err = auth.TryAuthentication(authUsername, authPassword); err != nil {
|
||||
var authErr auth.AuthError
|
||||
if errors.As(err, &authErr) {
|
||||
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
|
||||
return ctx.PlainText(404, "Check your credentials or make sure you have access to the Gist")
|
||||
}
|
||||
return ctx.ErrorRes(500, "Authentication system error", nil)
|
||||
}
|
||||
|
||||
urlPath := ctx.Request().URL.Path
|
||||
pathParts := strings.Split(strings.Trim(urlPath, "/"), "/")
|
||||
if pathParts[0] == authUsername && len(pathParts) == 4 {
|
||||
log.Debug().Str("authUsername", authUsername).Msg("Valid URL format for push operation")
|
||||
gist, err = createGist(user, pathParts[1])
|
||||
if err != nil {
|
||||
return ctx.ErrorRes(500, "Cannot create gist", err)
|
||||
}
|
||||
log.Debug().Str("authUsername", authUsername).Str("url", urlPath).Msg("Gist created")
|
||||
ctx.SetData("gist", gist)
|
||||
ctx.SetData("repositoryPath", git.RepositoryPath(gist.User.Username, gist.Uuid))
|
||||
} else {
|
||||
log.Debug().Str("authUsername", authUsername).Any("path", pathParts).Msg("Invalid URL format for push operation")
|
||||
return ctx.PlainText(401, "Invalid URL format for push operation")
|
||||
}
|
||||
return route.handler(ctx)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return route.handler(ctx)
|
||||
}
|
||||
|
||||
func findMatchingRoute(ctx *context.Context) *struct {
|
||||
gitUrl string
|
||||
method string
|
||||
handler func(ctx *context.Context) error
|
||||
} {
|
||||
for _, route := range routes {
|
||||
matched, _ := regexp.MatchString(route.gitUrl, ctx.Request().URL.Path)
|
||||
if ctx.Request().Method == route.method && matched {
|
||||
if !strings.HasPrefix(ctx.Request().Header.Get("User-Agent"), "git/") {
|
||||
continue
|
||||
}
|
||||
|
||||
gist := ctx.GetData("gist").(*db.Gist)
|
||||
|
||||
isInit := strings.HasPrefix(ctx.Request().URL.Path, "/init/info/refs")
|
||||
isInitReceive := strings.HasPrefix(ctx.Request().URL.Path, "/init/git-receive-pack")
|
||||
isInfoRefs := strings.HasSuffix(route.gitUrl, "/info/refs$")
|
||||
isPull := ctx.QueryParam("service") == "git-upload-pack" ||
|
||||
strings.HasSuffix(ctx.Request().URL.Path, "git-upload-pack") ||
|
||||
ctx.Request().Method == "GET" && !isInfoRefs
|
||||
|
||||
repositoryPath := git.RepositoryPath(gist.User.Username, gist.Uuid)
|
||||
if _, err := os.Stat(repositoryPath); os.IsNotExist(err) {
|
||||
if err != nil {
|
||||
log.Info().Err(err).Msg("Repository directory does not exist")
|
||||
return ctx.ErrorRes(404, "Repository directory does not exist", err)
|
||||
}
|
||||
}
|
||||
|
||||
ctx.SetData("repositoryPath", repositoryPath)
|
||||
|
||||
allow, err := auth.ShouldAllowUnauthenticatedGistAccess(handlers.ContextAuthInfo{Context: ctx}, true)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Cannot check if unauthenticated access is allowed")
|
||||
}
|
||||
|
||||
// Shows basic auth if :
|
||||
// - user wants to push the gist
|
||||
// - user wants to clone/pull a private gist
|
||||
// - gist is not found (obfuscation)
|
||||
// - admin setting to require login is set to true
|
||||
if isPull && gist.Private != db.PrivateVisibility && gist.ID != 0 && allow {
|
||||
return route.handler(ctx)
|
||||
}
|
||||
|
||||
authHeader := ctx.Request().Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
return basicAuth(ctx)
|
||||
}
|
||||
|
||||
authFields := strings.Fields(authHeader)
|
||||
if len(authFields) != 2 || authFields[0] != "Basic" {
|
||||
return basicAuth(ctx)
|
||||
}
|
||||
|
||||
authUsername, authPassword, err := basicAuthDecode(authFields[1])
|
||||
if err != nil {
|
||||
return basicAuth(ctx)
|
||||
}
|
||||
|
||||
if !isInit && !isInitReceive {
|
||||
if gist.ID == 0 {
|
||||
return ctx.PlainText(404, "Check your credentials or make sure you have access to the Gist")
|
||||
}
|
||||
|
||||
var userToCheckPermissions *db.User
|
||||
if gist.Private != db.PrivateVisibility && isPull {
|
||||
userToCheckPermissions, _ = db.GetUserByUsername(authUsername)
|
||||
} else {
|
||||
userToCheckPermissions = &gist.User
|
||||
}
|
||||
|
||||
// ldap
|
||||
ldapSuccess := false
|
||||
if ldap.Enabled() {
|
||||
if ok, err := ldap.Authenticate(userToCheckPermissions.Username, authPassword); !ok {
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("LDAP authentication error")
|
||||
}
|
||||
log.Warn().Msg("Invalid LDAP authentication attempt from " + ctx.RealIP())
|
||||
} else {
|
||||
ldapSuccess = true
|
||||
}
|
||||
}
|
||||
|
||||
// password
|
||||
if !ldapSuccess {
|
||||
if ok, err := password.VerifyPassword(authPassword, userToCheckPermissions.Password); !ok {
|
||||
if err != nil {
|
||||
return ctx.ErrorRes(500, "Cannot verify password", err)
|
||||
}
|
||||
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
|
||||
return ctx.PlainText(404, "Check your credentials or make sure you have access to the Gist")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var user *db.User
|
||||
if user, err = db.GetUserByUsername(authUsername); err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return ctx.ErrorRes(500, "Cannot get user", err)
|
||||
}
|
||||
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
|
||||
return ctx.ErrorRes(401, "Invalid credentials", nil)
|
||||
}
|
||||
ldapSuccess := false
|
||||
if ldap.Enabled() {
|
||||
if ok, err := ldap.Authenticate(user.Username, authPassword); !ok {
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("LDAP authentication error")
|
||||
}
|
||||
log.Warn().Msg("Invalid LDAP authentication attempt from " + ctx.RealIP())
|
||||
} else {
|
||||
ldapSuccess = true
|
||||
}
|
||||
}
|
||||
if !ldapSuccess {
|
||||
if ok, err := password.VerifyPassword(authPassword, user.Password); !ok {
|
||||
if err != nil {
|
||||
return ctx.ErrorRes(500, "Cannot check for password", err)
|
||||
}
|
||||
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
|
||||
return ctx.ErrorRes(401, "Invalid credentials", nil)
|
||||
}
|
||||
}
|
||||
|
||||
if isInit {
|
||||
gist = new(db.Gist)
|
||||
gist.UserID = user.ID
|
||||
gist.User = *user
|
||||
uuidGist, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return ctx.ErrorRes(500, "Error creating an UUID", err)
|
||||
}
|
||||
gist.Uuid = strings.Replace(uuidGist.String(), "-", "", -1)
|
||||
gist.Title = "gist:" + gist.Uuid
|
||||
|
||||
if err = gist.InitRepository(); err != nil {
|
||||
return ctx.ErrorRes(500, "Cannot init repository in the file system", err)
|
||||
}
|
||||
|
||||
if err = gist.Create(); err != nil {
|
||||
return ctx.ErrorRes(500, "Cannot init repository in database", err)
|
||||
}
|
||||
|
||||
err = db.AddInitGistToQueue(gist.ID, user.ID)
|
||||
if err != nil {
|
||||
return ctx.ErrorRes(500, "Cannot add inited gist to the queue", err)
|
||||
}
|
||||
ctx.SetData("gist", gist)
|
||||
} else {
|
||||
gist, err = db.GetInitGistInQueueForUser(user.ID)
|
||||
if err != nil {
|
||||
return ctx.ErrorRes(500, "Cannot retrieve inited gist from the queue", err)
|
||||
}
|
||||
|
||||
ctx.SetData("gist", gist)
|
||||
ctx.SetData("repositoryPath", git.RepositoryPath(gist.User.Username, gist.Uuid))
|
||||
}
|
||||
}
|
||||
|
||||
return route.handler(ctx)
|
||||
return &route
|
||||
}
|
||||
}
|
||||
return ctx.NotFound("Gist not found")
|
||||
return nil
|
||||
}
|
||||
|
||||
func createGist(user *db.User, url string) (*db.Gist, error) {
|
||||
gist := new(db.Gist)
|
||||
gist.UserID = user.ID
|
||||
gist.User = *user
|
||||
uuidGist, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gist.Uuid = strings.Replace(uuidGist.String(), "-", "", -1)
|
||||
gist.Title = "gist:" + gist.Uuid
|
||||
|
||||
if url != "" {
|
||||
gist.URL = strings.TrimSuffix(url, ".git")
|
||||
gist.Title = strings.TrimSuffix(url, ".git")
|
||||
}
|
||||
|
||||
if err := gist.InitRepository(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := gist.Create(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return gist, nil
|
||||
}
|
||||
|
||||
func uploadPack(ctx *context.Context) error {
|
||||
@@ -331,6 +373,26 @@ func basicAuth(ctx *context.Context) error {
|
||||
return ctx.PlainText(401, "Requires authentication")
|
||||
}
|
||||
|
||||
func parseAuthHeader(ctx *context.Context) (string, string, error) {
|
||||
authHeader := ctx.Request().Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
return "", "", errors.New("no auth header")
|
||||
}
|
||||
|
||||
authFields := strings.Fields(authHeader)
|
||||
if len(authFields) != 2 || authFields[0] != "Basic" {
|
||||
return "", "", errors.New("invalid auth header")
|
||||
}
|
||||
|
||||
authUsername, authPassword, err := basicAuthDecode(authFields[1])
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Cannot decode basic auth header")
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return authUsername, authPassword, nil
|
||||
}
|
||||
|
||||
func basicAuthDecode(encoded string) (string, string, error) {
|
||||
s, err := base64.StdEncoding.DecodeString(encoded)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user