diff --git a/README.md b/README.md index 30253ac..3d6a0c5 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ A self-hosted pastebin **powered by Git**. [Try it here](https://opengist.thomic ## Features -* Create public or unlisted snippets +* Create public, unlisted or private snippets * Clone / Pull / Push snippets **via Git** over HTTP or SSH * Revisions history * Syntax highlighting ; markdown & CSV support diff --git a/internal/models/gist.go b/internal/models/gist.go index 3bcd1da..84e7591 100644 --- a/internal/models/gist.go +++ b/internal/models/gist.go @@ -15,7 +15,7 @@ type Gist struct { Preview string PreviewFilename string Description string - Private bool + Private int // 0: public, 1: unlisted, 2: private UserID uint User User NbFiles int @@ -89,7 +89,7 @@ func GetAllGists(offset int) ([]*Gist, error) { func GetAllGistsFromSearch(currentUserId uint, query string, offset int, sort string, order string) ([]*Gist, error) { var gists []*Gist err := db.Preload("User").Preload("Forked.User"). - Where("((gists.private = 0) or (gists.private = 1 and gists.user_id = ?))", currentUserId). + Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId). Where("gists.title like ? or gists.description like ?", "%"+query+"%", "%"+query+"%"). Limit(11). Offset(offset * 10). @@ -101,7 +101,7 @@ func GetAllGistsFromSearch(currentUserId uint, query string, offset int, sort st func gistsFromUserStatement(fromUserId uint, currentUserId uint) *gorm.DB { return db.Preload("User").Preload("Forked.User"). - Where("((gists.private = 0) or (gists.private = 1 and gists.user_id = ?))", currentUserId). + Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId). Where("users.id = ?", fromUserId). Joins("join users on gists.user_id = users.id") } @@ -124,7 +124,7 @@ func CountAllGistsFromUser(fromUserId uint, currentUserId uint) (int64, error) { func likedStatement(fromUserId uint, currentUserId uint) *gorm.DB { return db.Preload("User").Preload("Forked.User"). - Where("((gists.private = 0) or (gists.private = 1 and gists.user_id = ?))", currentUserId). + Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId). Where("likes.user_id = ?", fromUserId). Joins("join likes on gists.id = likes.gist_id"). Joins("join users on likes.user_id = users.id") @@ -147,7 +147,7 @@ func CountAllGistsLikedByUser(fromUserId uint, currentUserId uint) (int64, error func forkedStatement(fromUserId uint, currentUserId uint) *gorm.DB { return db.Preload("User").Preload("Forked.User"). - Where("gists.forked_id is not null and ((gists.private = 0) or (gists.private = 1 and gists.user_id = ?))", currentUserId). + Where("gists.forked_id is not null and ((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId). Where("gists.user_id = ?", fromUserId). Joins("join users on gists.user_id = users.id") } @@ -243,7 +243,7 @@ func (gist *Gist) GetForks(currentUserId uint, offset int) ([]*Gist, error) { var gists []*Gist err := db.Model(&gist).Preload("User"). Where("forked_id = ?", gist.ID). - Where("(gists.private = 0) or (gists.private = 1 and gists.user_id = ?)", currentUserId). + Where("(gists.private = 0) or (gists.private > 0 and gists.user_id = ?)", currentUserId). Limit(11). Offset(offset * 10). Order("updated_at desc"). @@ -379,7 +379,7 @@ func (gist *Gist) UpdatePreviewAndCount() error { type GistDTO struct { Title string `validate:"max=50" form:"title"` Description string `validate:"max=150" form:"description"` - Private bool `form:"private"` + Private int `validate:"number,min=0,max=2" form:"private"` Files []FileDTO `validate:"min=1,dive"` } diff --git a/internal/ssh/git_ssh.go b/internal/ssh/git_ssh.go index 54ca59b..e8ade42 100644 --- a/internal/ssh/git_ssh.go +++ b/internal/ssh/git_ssh.go @@ -42,12 +42,21 @@ func runGitCommand(ch ssh.Channel, gitCmd string, key string, ip string) error { return errors.New("internal server error") } - if verb == "receive-pack" || requireLogin == "1" { + // Check for the key if : + // - user wants to push the gist + // - user wants to clone a private gist + // - gist is not found (obfuscation) + // - admin setting to require login is set to true + if verb == "receive-pack" || + gist.Private == 2 || + gist.ID == 0 || + requireLogin == "1" { + pubKey, err := models.SSHKeyExistsForUser(key, gist.UserID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { log.Warn().Msg("Invalid SSH authentication attempt from " + ip) - return errors.New("unauthorized") + return errors.New("gist not found") } errorSsh("Failed to get user by SSH key id", err) return errors.New("internal server error") diff --git a/internal/web/gist.go b/internal/web/gist.go index 6080fde..6a44623 100644 --- a/internal/web/gist.go +++ b/internal/web/gist.go @@ -80,7 +80,7 @@ func gistInit(next echo.HandlerFunc) echo.HandlerFunc { setData(ctx, "hasLiked", hasLiked) } - if gist.Private { + if gist.Private > 0 { setData(ctx, "NoIndex", true) } @@ -88,6 +88,22 @@ func gistInit(next echo.HandlerFunc) echo.HandlerFunc { } } +// gistSoftInit try to load a gist (same as gistInit) but does not return a 404 if the gist is not found +// useful for git clients using HTTP to obfuscate the existence of a private gist +func gistSoftInit(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctx echo.Context) error { + userName := ctx.Param("user") + gistName := ctx.Param("gistname") + + gistName = strings.TrimSuffix(gistName, ".git") + + gist, _ := models.GetGist(userName, gistName) + setData(ctx, "gist", gist) + + return next(ctx) + } +} + func allGists(ctx echo.Context) error { var err error var urlPage string @@ -400,7 +416,7 @@ func processCreate(ctx echo.Context) error { func toggleVisibility(ctx echo.Context) error { var gist = getData(ctx, "gist").(*models.Gist) - gist.Private = !gist.Private + gist.Private = (gist.Private + 1) % 3 if err := gist.Update(); err != nil { return errorRes(500, "Error updating this gist", err) } diff --git a/internal/web/git_http.go b/internal/web/git_http.go index 54e0649..61b01a0 100644 --- a/internal/web/git_http.go +++ b/internal/web/git_http.go @@ -47,16 +47,23 @@ func gitHttp(ctx echo.Context) error { gist := getData(ctx, "gist").(*models.Gist) + // Shows basic auth if : + // - user wants to push the gist + // - user wants to clone a private gist + // - gist is not found (obfuscation) + // - admin setting to require login is set to true noAuth := (ctx.QueryParam("service") == "git-upload-pack" || strings.HasSuffix(ctx.Request().URL.Path, "git-upload-pack") || ctx.Request().Method == "GET") && + gist.Private != 2 && + gist.ID != 0 && !getData(ctx, "RequireLogin").(bool) repositoryPath := git.RepositoryPath(gist.User.Username, gist.Uuid) if _, err := os.Stat(repositoryPath); os.IsNotExist(err) { if err != nil { - return errorRes(500, "Repository does not exist", err) + return errorRes(404, "Repository directory does not exist", err) } } @@ -82,12 +89,16 @@ func gitHttp(ctx echo.Context) error { return basicAuth(ctx) } + if gist.ID == 0 { + return errorRes(404, "Not found", nil) + } + if ok, err := argon2id.verify(authPassword, gist.User.Password); !ok || gist.User.Username != authUsername { if err != nil { return errorRes(500, "Cannot verify password", err) } log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP()) - return errorRes(403, "Unauthorized", nil) + return errorRes(404, "Not found", nil) } return route.handler(ctx) diff --git a/internal/web/run.go b/internal/web/run.go index 14e8b9c..3b96b28 100644 --- a/internal/web/run.go +++ b/internal/web/run.go @@ -30,11 +30,11 @@ var re = regexp.MustCompile("[^a-z0-9]+") var fm = template.FuncMap{ "split": strings.Split, "indexByte": strings.IndexByte, - "toInt": func(i string) int64 { - val, _ := strconv.ParseInt(i, 10, 64) + "toInt": func(i string) int { + val, _ := strconv.Atoi(i) return val }, - "inc": func(i int64) int64 { + "inc": func(i int) int { return i + 1 }, "splitGit": func(i string) []string { @@ -88,6 +88,20 @@ var fm = template.FuncMap{ return config.C.ExternalUrl + "/" + manifestEntries[jsfile].File }, "defaultAvatar": defaultAvatar, + "visibilityStr": func(visibility int, lowercase bool) string { + s := "Public" + switch visibility { + case 1: + s = "Unlisted" + case 2: + s = "Private" + } + + if lowercase { + return strings.ToLower(s) + } + return s + }, } var EmbedFS fs.FS @@ -226,7 +240,7 @@ func Start() { debugStr := "" // Git HTTP routes if config.C.HttpGit { - e.Any("/:user/:gistname/*", gitHttp, gistInit) + e.Any("/:user/:gistname/*", gitHttp, gistSoftInit) debugStr = " (with Git over HTTP)" } diff --git a/public/main.ts b/public/main.ts index c354e35..0adb138 100644 --- a/public/main.ts +++ b/public/main.ts @@ -183,4 +183,20 @@ document.addEventListener('DOMContentLoaded', () => { }); }; }); + + const gistmenuvisibility = document.getElementById('gist-menu-visibility'); + if (gistmenuvisibility) { + let submitgistbutton = (document.getElementById('submit-gist') as HTMLInputElement); + document.getElementById('gist-visibility-menu-button')!.onclick = () => { + console.log("z"); + gistmenuvisibility!.classList.toggle('hidden'); + } + Array.from(document.querySelectorAll('.gist-visibility-option')).forEach((el) => { + (el as HTMLElement).onclick = () => { + submitgistbutton.textContent = "Create " + el.textContent.toLowerCase() + " gist"; + submitgistbutton!.value = (el as HTMLElement).dataset.visibility || '0'; + gistmenuvisibility!.classList.add('hidden'); + } + }); + } }); diff --git a/templates/base/gist_header.html b/templates/base/gist_header.html index f0f5d60..cffadbe 100644 --- a/templates/base/gist_header.html +++ b/templates/base/gist_header.html @@ -92,8 +92,7 @@

Forked from {{ .gist.Forked.User.Username }}/{{ .gist.Forked.Title }}

{{ end }}

Last active {{ .gist.UpdatedAt }} - {{ if .gist.Private }} • Unlisted {{ end }} - + {{ if .gist.Private }} • {{ visibilityStr .gist.Private false }} {{ end }}

{{ .gist.Description }}

diff --git a/templates/pages/all.html b/templates/pages/all.html index 6e8402d..d5b619d 100644 --- a/templates/pages/all.html +++ b/templates/pages/all.html @@ -137,7 +137,7 @@
Last active {{ $gist.UpdatedAt }} {{ if $gist.Forked }} • Forked from {{ $gist.Forked.User.Username }}/{{ $gist.Forked.Title }} {{ end }} - {{ if $gist.Private }} • Unlisted {{ end }}
+ {{ if $gist.Private }} • {{ visibilityStr $gist.Private false }} {{ end }}
{{ $gist.Description }}
diff --git a/templates/pages/create.html b/templates/pages/create.html index a0c2169..4dc0dbc 100644 --- a/templates/pages/create.html +++ b/templates/pages/create.html @@ -56,8 +56,25 @@
- - + +
+ +
+ + +
+
{{ .csrfHtml }} diff --git a/templates/pages/edit.html b/templates/pages/edit.html index fb7f868..2aa05dd 100644 --- a/templates/pages/edit.html +++ b/templates/pages/edit.html @@ -11,18 +11,17 @@
{{ .csrfHtml }}