Init gist with regular urls via git CLI (http) (#501)

This commit is contained in:
Thomas Miceli
2025-08-28 02:44:09 +02:00
committed by GitHub
parent 2976173658
commit 905276f24b
14 changed files with 522 additions and 307 deletions

View File

@@ -50,7 +50,7 @@ watch_backend:
OG_DEV=1 npx nodemon --watch '**/*' -e html,yml,go,js --signal SIGTERM --exec 'go run -ldflags "-X $(VERSION_PKG)=$(GIT_TAG)" . --config config.yml'
watch:
@sh ./scripts/watch.sh
@bash ./scripts/watch.sh
clean:
@echo "Cleaning up build artifacts..."

View File

@@ -3,7 +3,7 @@
# https://github.com/thomiceli/opengist/blob/master/docs/configuration/cheat-sheet.md
# Set the log level to one of the following: debug, info, warn, error, fatal. Default: warn
log-level: warn
log-level: debug
# Set the log output to one or more of the following: `stdout`, `file`. Default: stdout,file
log-output: stdout,file

View File

@@ -1,6 +1,6 @@
# Init Gists via Git
Opengist allows you to create new snippets via Git over HTTP.
Opengist allows you to create new snippets via Git over HTTP. You can create gists with either auto-generated URLs or custom URLs of your choice.
Simply init a new Git repository where your file(s) is/are located:
@@ -10,19 +10,41 @@ git add .
git commit -m "My cool snippet"
```
Then add this Opengist special remote URL and push your changes:
### Option A: Regular URL
Create a gist with a custom URL using the format `http://opengist.url/username/custom-url`, where `username` is your authenticated username and `custom-url` is your desired gist identifier.
The gist must not exist yet if you want to create it, otherwise you will just push to the existing gist.
```shell
git remote add origin http://localhost:6157/init
git remote add origin http://opengist.url/thomas/my-custom-gist
git push -u origin master
```
Log in with your Opengist account credentials, and your snippet will be created at the specified URL:
**Requirements for custom URLs:**
- The username must match your authenticated username
- URL format: `http://opengist.url/username/custom-url`
- The custom URL becomes your gist's identifier and title
- `.git` suffix is automatically removed if present
### Option B: Init endpoint
Use the special `http://opengist.url/init` endpoint to create a gist with an automatically generated URL:
```shell
Username for 'http://localhost:6157': thomas
Password for 'http://thomas@localhost:6157':
git remote add origin http://opengist.url/init
git push -u origin master
```
## Authentication
When you push, you'll be prompted to authenticate:
```shell
Username for 'http://opengist.url': thomas
Password for 'http://thomas@opengist.url': [your-password]
Enumerating objects: 3, done.
Counting objects: 100% (3/3), done.
Delta compression using up to 8 threads
@@ -30,12 +52,12 @@ Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 416 bytes | 416.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
remote:
remote: Your new repository has been created here: http://localhost:6157/thomas/6051e930f140429f9a2f3bb1fa101066
remote: Your new repository has been created here: http://opengist.url/thomas/6051e930f140429f9a2f3bb1fa101066
remote:
remote: If you want to keep working with your gist, you could set the remote URL via:
remote: git remote set-url origin http://localhost:6157/thomas/6051e930f140429f9a2f3bb1fa101066
remote: git remote set-url origin http://opengist.url/thomas/6051e930f140429f9a2f3bb1fa101066
remote:
To http://localhost:6157/init
To http://opengist.url/init
* [new branch] master -> master
```

View File

@@ -1,4 +1,4 @@
package auth
package password
import (
"crypto/rand"
@@ -6,8 +6,9 @@ import (
"encoding/base64"
"errors"
"fmt"
"golang.org/x/crypto/argon2"
"strings"
"golang.org/x/crypto/argon2"
)
type argon2ID struct {

View File

@@ -1,11 +1,9 @@
package password
import "github.com/thomiceli/opengist/internal/auth"
func HashPassword(code string) (string, error) {
return auth.Argon2id.Hash(code)
return Argon2id.Hash(code)
}
func VerifyPassword(code, hashedCode string) (bool, error) {
return auth.Argon2id.Verify(code, hashedCode)
return Argon2id.Verify(code, hashedCode)
}

View File

@@ -1,4 +1,4 @@
package auth
package totp
import (
"crypto/aes"

View File

@@ -0,0 +1,83 @@
package auth
import (
"errors"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/auth/ldap"
passwordpkg "github.com/thomiceli/opengist/internal/auth/password"
"github.com/thomiceli/opengist/internal/db"
"gorm.io/gorm"
)
type AuthError struct {
message string
}
func (e AuthError) Error() string {
return e.message
}
func TryAuthentication(username, password string) (*db.User, error) {
user, err := db.GetUserByUsername(username)
if err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
log.Error().Err(err).Msgf("Cannot get user by username %s", username)
return nil, err
}
}
if user.Password != "" {
return tryDbLogin(user, password)
} else {
if ldap.Enabled() {
return tryLdapLogin(username, password)
}
return nil, AuthError{"no authentication method available"}
}
}
func tryDbLogin(user *db.User, password string) (*db.User, error) {
if ok, err := passwordpkg.VerifyPassword(password, user.Password); !ok {
if err != nil {
log.Error().Err(err).Msg("Password verification failed")
return nil, err
}
return nil, AuthError{"invalid password"}
}
return user, nil
}
func tryLdapLogin(username, password string) (user *db.User, err error) {
ok, err := ldap.Authenticate(username, password)
if err != nil {
log.Error().Err(err).Msg("LDAP authentication failed")
return nil, err
}
if !ok {
return nil, AuthError{"invalid LDAP credentials"}
}
if user, err = db.GetUserByUsername(username); err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
log.Error().Err(err).Msgf("Cannot get user by username %s", username)
return nil, err
}
}
if errors.Is(err, gorm.ErrRecordNotFound) {
user = &db.User{
Username: username,
}
if err = user.Create(); err != nil {
log.Warn().Err(err).Msg("Cannot create user after LDAP authentication")
return nil, err
}
return user, nil
}
return user, nil
}

View File

@@ -269,5 +269,5 @@ func DeprecationDBFilename() {
}
func TruncateDatabase() error {
return db.Migrator().DropTable("likes", &User{}, "gists", &SSHKey{}, &AdminSetting{}, &Invitation{}, &WebAuthnCredential{}, &TOTP{}, &GistTopic{}, &GistLanguage{})
return db.Migrator().DropTable("likes", &User{}, "gists", &SSHKey{}, &AdminSetting{}, &Invitation{}, &WebAuthnCredential{}, &TOTP{}, &GistTopic{}, &GistLanguage{}, &GistInitQueue{})
}

View File

@@ -6,11 +6,11 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"github.com/thomiceli/opengist/internal/auth"
"slices"
"github.com/thomiceli/opengist/internal/auth/password"
ogtotp "github.com/thomiceli/opengist/internal/auth/totp"
"github.com/thomiceli/opengist/internal/config"
"slices"
)
type TOTP struct {
@@ -31,7 +31,7 @@ func GetTOTPByUserID(userID uint) (*TOTP, error) {
func (totp *TOTP) StoreSecret(secret string) error {
secretBytes := []byte(secret)
encrypted, err := auth.AESEncrypt(config.SecretKey, secretBytes)
encrypted, err := ogtotp.AESEncrypt(config.SecretKey, secretBytes)
if err != nil {
return err
}
@@ -46,7 +46,7 @@ func (totp *TOTP) ValidateCode(code string) (bool, error) {
return false, err
}
secretBytes, err := auth.AESDecrypt(config.SecretKey, ciphertext)
secretBytes, err := ogtotp.AESDecrypt(config.SecretKey, ciphertext)
if err != nil {
return false, err
}

View File

@@ -2,15 +2,16 @@ package context
import (
"context"
"html/template"
"net/http"
"sync"
"github.com/gorilla/sessions"
"github.com/labstack/echo/v4"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/i18n"
"html/template"
"net/http"
"sync"
)
type dataKey string
@@ -57,7 +58,7 @@ func (ctx *Context) DataMap() echo.Map {
}
func (ctx *Context) ErrorRes(code int, message string, err error) error {
if code >= 500 {
if code >= 500 && err != nil {
var skipLogger = log.With().CallerWithSkipFrameCount(3).Logger()
skipLogger.Error().Err(err).Msg(message)
}

View File

@@ -4,7 +4,7 @@ import (
"errors"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/auth/ldap"
"github.com/thomiceli/opengist/internal/auth"
passwordpkg "github.com/thomiceli/opengist/internal/auth/password"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/i18n"
@@ -125,24 +125,15 @@ func ProcessLogin(ctx *context.Context) error {
return ctx.ErrorRes(400, ctx.Tr("error.cannot-bind-data"), err)
}
localUser, err := db.GetUserByUsername(dto.Username)
hasLocalPassword := err == nil && localUser.Password != ""
if hasLocalPassword {
if user, err = tryDbLogin(ctx, dto.Username, dto.Password); user == nil {
return err
}
} else {
if ldap.Enabled() {
if user, err = tryLdapLogin(ctx, dto.Username, dto.Password); err != nil {
return err
}
}
if user == nil {
if user, err = tryDbLogin(ctx, dto.Username, dto.Password); user == nil {
return err
}
user, err = auth.TryAuthentication(dto.Username, dto.Password)
if err != nil {
var authErr auth.AuthError
if errors.As(err, &authErr) {
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
ctx.AddFlash(ctx.Tr("flash.auth.invalid-credentials"), "error")
return ctx.RedirectTo("/login")
}
return ctx.ErrorRes(500, "Authentication system error", nil)
}
// handle MFA
@@ -170,59 +161,3 @@ func Logout(ctx *context.Context) error {
ctx.DeleteCsrfCookie()
return ctx.RedirectTo("/all")
}
func tryDbLogin(ctx *context.Context, username, password string) (user *db.User, err error) {
if user, err = db.GetUserByUsername(username); err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ctx.ErrorRes(500, "Cannot get user", err)
}
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
ctx.AddFlash(ctx.Tr("flash.auth.invalid-credentials"), "error")
return nil, ctx.RedirectTo("/login")
}
if ok, err := passwordpkg.VerifyPassword(password, user.Password); !ok {
if err != nil {
return nil, ctx.ErrorRes(500, "Cannot check for password", err)
}
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
ctx.AddFlash(ctx.Tr("flash.auth.invalid-credentials"), "error")
return nil, ctx.RedirectTo("/login")
}
return user, nil
}
func tryLdapLogin(ctx *context.Context, username, password string) (user *db.User, err error) {
ok, err := ldap.Authenticate(username, password)
if err != nil {
log.Info().Err(err).Msgf("LDAP authentication error")
return nil, ctx.ErrorRes(500, "Cannot get user", err)
}
if !ok {
log.Warn().Msg("Invalid LDAP authentication attempt from " + ctx.RealIP())
return nil, nil
}
if user, err = db.GetUserByUsername(username); err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ctx.ErrorRes(500, "Cannot get user", err)
}
}
if errors.Is(err, gorm.ErrRecordNotFound) {
user = &db.User{
Username: username,
}
if err = user.Create(); err != nil {
log.Warn().Err(err).Msg("Cannot create user after LDAP authentication")
return nil, ctx.ErrorRes(500, "Cannot create user", err)
}
return user, nil
}
return user, nil
}

View File

@@ -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 {

View File

@@ -203,49 +203,28 @@ func TestGitOperations(t *testing.T) {
err = s.Request("POST", "/", gist3, 302)
require.NoError(t, err)
gitOperations := func(credentials, owner, url, filename string, expectErrorClone, expectErrorCheck, expectErrorPush bool) {
log.Debug().Msgf("Testing %s %s %t %t %t", credentials, url, expectErrorClone, expectErrorCheck, expectErrorPush)
err := clientGitClone(credentials, owner, url)
if expectErrorClone {
require.Error(t, err)
} else {
require.NoError(t, err)
}
err = clientCheckRepo(url, filename)
if expectErrorCheck {
require.Error(t, err)
} else {
require.NoError(t, err)
}
err = clientGitPush(url)
if expectErrorPush {
require.Error(t, err)
} else {
require.NoError(t, err)
}
}
tests := []struct {
credentials string
user string
url string
pushOptions string
expectErrorClone bool
expectErrorCheck bool
expectErrorPush bool
}{
{":", "kaguya", "kaguya-pub-gist", false, false, true},
{":", "kaguya", "kaguya-unl-gist", false, false, true},
{":", "kaguya", "kaguya-priv-gist", true, true, true},
{"kaguya:kaguya", "kaguya", "kaguya-pub-gist", false, false, false},
{"kaguya:kaguya", "kaguya", "kaguya-unl-gist", false, false, false},
{"kaguya:kaguya", "kaguya", "kaguya-priv-gist", false, false, false},
{"fujiwara:fujiwara", "kaguya", "kaguya-pub-gist", false, false, true},
{"fujiwara:fujiwara", "kaguya", "kaguya-unl-gist", false, false, true},
{"fujiwara:fujiwara", "kaguya", "kaguya-priv-gist", true, true, true},
{":", "kaguya", "kaguya-pub-gist", "", false, false, true},
{":", "kaguya", "kaguya-unl-gist", "", false, false, true},
{":", "kaguya", "kaguya-priv-gist", "", true, true, true},
{"kaguya:kaguya", "kaguya", "kaguya-pub-gist", "", false, false, false},
{"kaguya:kaguya", "kaguya", "kaguya-unl-gist", "", false, false, false},
{"kaguya:kaguya", "kaguya", "kaguya-priv-gist", "", false, false, false},
{"fujiwara:fujiwara", "kaguya", "kaguya-pub-gist", "", false, false, true},
{"fujiwara:fujiwara", "kaguya", "kaguya-unl-gist", "", false, false, true},
{"fujiwara:fujiwara", "kaguya", "kaguya-priv-gist", "", true, true, true},
}
for _, test := range tests {
gitOperations(test.credentials, test.user, test.url, "kaguya-file.txt", test.expectErrorClone, test.expectErrorCheck, test.expectErrorPush)
gitCloneCheckPush(t, test.credentials, test.user, test.url, "kaguya-file.txt", test.pushOptions, test.expectErrorClone, test.expectErrorCheck, test.expectErrorPush)
}
login(t, s, admin)
@@ -256,23 +235,24 @@ func TestGitOperations(t *testing.T) {
credentials string
user string
url string
pushOptions string
expectErrorClone bool
expectErrorCheck bool
expectErrorPush bool
}{
{":", "kaguya", "kaguya-pub-gist", true, true, true},
{":", "kaguya", "kaguya-unl-gist", true, true, true},
{":", "kaguya", "kaguya-priv-gist", true, true, true},
{"kaguya:kaguya", "kaguya", "kaguya-pub-gist", false, false, false},
{"kaguya:kaguya", "kaguya", "kaguya-unl-gist", false, false, false},
{"kaguya:kaguya", "kaguya", "kaguya-priv-gist", false, false, false},
{"fujiwara:fujiwara", "kaguya", "kaguya-pub-gist", false, false, true},
{"fujiwara:fujiwara", "kaguya", "kaguya-unl-gist", false, false, true},
{"fujiwara:fujiwara", "kaguya", "kaguya-priv-gist", true, true, true},
{":", "kaguya", "kaguya-pub-gist", "", true, true, true},
{":", "kaguya", "kaguya-unl-gist", "", true, true, true},
{":", "kaguya", "kaguya-priv-gist", "", true, true, true},
{"kaguya:kaguya", "kaguya", "kaguya-pub-gist", "", false, false, false},
{"kaguya:kaguya", "kaguya", "kaguya-unl-gist", "", false, false, false},
{"kaguya:kaguya", "kaguya", "kaguya-priv-gist", "", false, false, false},
{"fujiwara:fujiwara", "kaguya", "kaguya-pub-gist", "", false, false, true},
{"fujiwara:fujiwara", "kaguya", "kaguya-unl-gist", "", false, false, true},
{"fujiwara:fujiwara", "kaguya", "kaguya-priv-gist", "", true, true, true},
}
for _, test := range testsRequireLogin {
gitOperations(test.credentials, test.user, test.url, "kaguya-file.txt", test.expectErrorClone, test.expectErrorCheck, test.expectErrorPush)
gitCloneCheckPush(t, test.credentials, test.user, test.url, "kaguya-file.txt", test.pushOptions, test.expectErrorClone, test.expectErrorCheck, test.expectErrorPush)
}
login(t, s, admin)
@@ -280,31 +260,155 @@ func TestGitOperations(t *testing.T) {
require.NoError(t, err)
for _, test := range tests {
gitOperations(test.credentials, test.user, test.url, "kaguya-file.txt", test.expectErrorClone, test.expectErrorCheck, test.expectErrorPush)
gitCloneCheckPush(t, test.credentials, test.user, test.url, "kaguya-file.txt", test.pushOptions, test.expectErrorClone, test.expectErrorCheck, test.expectErrorPush)
}
}
func TestGitInit(t *testing.T) {
s := Setup(t)
defer Teardown(t, s)
admin := db.UserDTO{Username: "thomas", Password: "thomas"}
register(t, s, admin)
s.sessionCookie = ""
register(t, s, db.UserDTO{Username: "fujiwara", Password: "fujiwara"})
s.sessionCookie = ""
register(t, s, db.UserDTO{Username: "kaguya", Password: "kaguya"})
testsNewWithPush := []struct {
credentials string
user string
url string
pushOptions string
expectErrorClone bool
expectErrorCheck bool
expectErrorPush bool
}{
{":", "kaguya", "gist1", "", true, true, true},
{"kaguya:wrongpass", "kaguya", "gist2", "", true, true, true},
{"fujiwara:fujiwara", "kaguya", "gist3", "", true, true, true},
{"kaguya:kaguya", "kaguya", "gist4", "", false, false, false},
{"kaguya:kaguya", "kaguya", "gist5/g", "", true, true, true},
}
for _, test := range testsNewWithPush {
gitInitPush(t, test.credentials, test.user, test.url, "newfile.txt", test.pushOptions, test.expectErrorPush)
}
gist1db, err := db.GetGistByID("1")
require.NoError(t, err)
require.Equal(t, "kaguya", gist1db.User.Username)
for _, test := range testsNewWithPush {
gitCloneCheckPush(t, test.credentials, test.user, test.url, "newfile.txt", test.pushOptions, test.expectErrorClone, test.expectErrorCheck, test.expectErrorPush)
}
count, err := db.CountAll(db.Gist{})
require.NoError(t, err)
require.Equal(t, int64(1), count)
testsNewWithInit := []struct {
credentials string
url string
pushOptions string
expectErrorPush bool
}{
{":", "init", "", true},
{"fujiwara:wrongpass", "init", "", true},
{"kaguya:kaguya", "init", "", false},
{"fujiwara:fujiwara", "init", "", false},
}
for _, test := range testsNewWithInit {
gitInitPush(t, test.credentials, "kaguya", test.url, "newfile.txt", test.pushOptions, test.expectErrorPush)
}
count, err = db.CountAll(db.Gist{})
require.NoError(t, err)
require.Equal(t, int64(3), count)
gist2db, err := db.GetGistByID("2")
require.NoError(t, err)
require.Equal(t, "kaguya", gist2db.User.Username)
gist3db, err := db.GetGistByID("3")
require.NoError(t, err)
require.Equal(t, "fujiwara", gist3db.User.Username)
}
func clientGitClone(creds string, user string, url string) error {
return exec.Command("git", "clone", "http://"+creds+"@localhost:6157/"+user+"/"+url, filepath.Join(config.GetHomeDir(), "tmp", url)).Run()
}
func clientGitPush(url string) error {
f, err := os.Create(filepath.Join(config.GetHomeDir(), "tmp", url, "newfile.txt"))
func clientGitPush(url string, pushOptions string, file string) error {
f, err := os.Create(filepath.Join(config.GetHomeDir(), "tmp", url, file))
if err != nil {
return err
}
f.Close()
_, _ = f.WriteString("new file")
_ = f.Close()
_ = exec.Command("git", "-C", filepath.Join(config.GetHomeDir(), "tmp", url), "add", "newfile.txt").Run()
_ = exec.Command("git", "-C", filepath.Join(config.GetHomeDir(), "tmp", url), "add", file).Run()
_ = exec.Command("git", "-C", filepath.Join(config.GetHomeDir(), "tmp", url), "commit", "-m", "new file").Run()
err = exec.Command("git", "-C", filepath.Join(config.GetHomeDir(), "tmp", url), "push", "origin", "master").Run()
if pushOptions != "" {
err = exec.Command("git", "-C", filepath.Join(config.GetHomeDir(), "tmp", url), "push", pushOptions, "origin").Run()
} else {
err = exec.Command("git", "-C", filepath.Join(config.GetHomeDir(), "tmp", url), "push", "origin").Run()
}
_ = os.RemoveAll(filepath.Join(config.GetHomeDir(), "tmp", url))
return err
}
func clientGitInit(path string) error {
return exec.Command("git", "init", "--initial-branch=master", filepath.Join(config.GetHomeDir(), "tmp", path)).Run()
}
func clientGitSetRemote(path string, remoteName string, remoteUrl string) error {
return exec.Command("git", "-C", filepath.Join(config.GetHomeDir(), "tmp", path), "remote", "add", remoteName, remoteUrl).Run()
}
func clientCheckRepo(url string, file string) error {
_, err := os.ReadFile(filepath.Join(config.GetHomeDir(), "tmp", url, file))
return err
}
func gitCloneCheckPush(t *testing.T, credentials, owner, url, filename, pushOptions string, expectErrorClone, expectErrorCheck, expectErrorPush bool) {
log.Debug().Msgf("Testing %s %s %t %t %t", credentials, url, expectErrorClone, expectErrorCheck, expectErrorPush)
err := clientGitClone(credentials, owner, url)
if expectErrorClone {
require.Error(t, err)
} else {
require.NoError(t, err)
}
err = clientCheckRepo(url, filename)
if expectErrorCheck {
require.Error(t, err)
} else {
require.NoError(t, err)
}
err = clientGitPush(url, pushOptions, filename)
if expectErrorPush {
require.Error(t, err)
} else {
require.NoError(t, err)
}
}
func gitInitPush(t *testing.T, credentials, owner, url, filename, pushOptions string, expectErrorPush bool) {
log.Debug().Msgf("Testing %s %s %t", credentials, url, expectErrorPush)
err := clientGitInit(url)
require.NoError(t, err)
if url == "init" {
err = clientGitSetRemote(url, "origin", "http://"+credentials+"@localhost:6157/init/")
} else {
err = clientGitSetRemote(url, "origin", "http://"+credentials+"@localhost:6157/"+owner+"/"+url)
}
require.NoError(t, err)
err = clientGitPush(url, pushOptions, filename)
if expectErrorPush {
require.Error(t, err)
} else {
require.NoError(t, err)
}
}

View File

@@ -8,6 +8,7 @@ import (
"net/http/httptest"
"net/url"
"os"
"os/exec"
"path/filepath"
"reflect"
"runtime"
@@ -153,8 +154,16 @@ func Setup(t *testing.T) *TestServer {
config.C.Index = ""
config.C.LogLevel = "error"
config.C.GitDefaultBranch = "master"
config.InitLog()
err = exec.Command("git", "config", "--global", "--type", "bool", "push.autoSetupRemote", "true").Run()
require.NoError(t, err)
err = exec.Command("git", "config", "--global", "user.email", "test@opengist.io").Run()
require.NoError(t, err)
err = exec.Command("git", "config", "--global", "user.name", "test").Run()
require.NoError(t, err)
homePath := config.GetHomeDir()
log.Info().Msg("Data directory: " + homePath)