diff options
author | Anthony Wang | 2023-02-11 23:52:36 +0000 |
---|---|---|
committer | Anthony Wang | 2023-02-11 23:52:36 +0000 |
commit | e61e9fba59d6e6f0eb86869e07d9d52867384132 (patch) | |
tree | c6072319ab1b1429eb4132c9797ab4c6d6ffd760 /modules | |
parent | 1a54d5e8970f2ff6ffe3aeaa19b3917f5e7dc9fd (diff) | |
parent | 1cb8d14bf71e0b8637c9eaa10808b4fd05139f45 (diff) |
Merge remote-tracking branch 'origin/main' into forgejo-federation
Diffstat (limited to 'modules')
39 files changed, 850 insertions, 391 deletions
diff --git a/modules/auth/common.go b/modules/auth/common.go new file mode 100644 index 000000000..77361f656 --- /dev/null +++ b/modules/auth/common.go @@ -0,0 +1,22 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package auth + +import ( + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/log" +) + +func UnmarshalGroupTeamMapping(raw string) (map[string]map[string][]string, error) { + groupTeamMapping := make(map[string]map[string][]string) + if raw == "" { + return groupTeamMapping, nil + } + err := json.Unmarshal([]byte(raw), &groupTeamMapping) + if err != nil { + log.Error("Failed to unmarshal group team mapping: %v", err) + return nil, err + } + return groupTeamMapping, nil +} diff --git a/modules/charset/escape_stream.go b/modules/charset/escape_stream.go index 823b63513..1b956bf4a 100644 --- a/modules/charset/escape_stream.go +++ b/modules/charset/escape_stream.go @@ -6,7 +6,6 @@ package charset import ( "fmt" "regexp" - "sort" "strings" "unicode" "unicode/utf8" @@ -20,12 +19,16 @@ import ( var defaultWordRegexp = regexp.MustCompile(`(-?\d*\.\d\w*)|([^\` + "`" + `\~\!\@\#\$\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s\x00-\x1f]+)`) func NewEscapeStreamer(locale translation.Locale, next HTMLStreamer, allowed ...rune) HTMLStreamer { + allowedM := make(map[rune]bool, len(allowed)) + for _, v := range allowed { + allowedM[v] = true + } return &escapeStreamer{ escaped: &EscapeStatus{}, PassthroughHTMLStreamer: *NewPassthroughStreamer(next), locale: locale, ambiguousTables: AmbiguousTablesForLocale(locale), - allowed: allowed, + allowed: allowedM, } } @@ -34,7 +37,7 @@ type escapeStreamer struct { escaped *EscapeStatus locale translation.Locale ambiguousTables []*AmbiguousTable - allowed []rune + allowed map[rune]bool } func (e *escapeStreamer) EscapeStatus() *EscapeStatus { @@ -256,7 +259,7 @@ func (e *escapeStreamer) runeTypes(runes ...rune) (types []runeType, confusables runeCounts.numBrokenRunes++ case r == ' ' || r == '\t' || r == '\n': runeCounts.numBasicRunes++ - case e.isAllowed(r): + case e.allowed[r]: if r > 0x7e || r < 0x20 { types[i] = nonBasicASCIIRuneType runeCounts.numNonConfusingNonBasicRunes++ @@ -282,16 +285,3 @@ func (e *escapeStreamer) runeTypes(runes ...rune) (types []runeType, confusables } return types, confusables, runeCounts } - -func (e *escapeStreamer) isAllowed(r rune) bool { - if len(e.allowed) == 0 { - return false - } - if len(e.allowed) == 1 { - return e.allowed[0] == r - } - - return sort.Search(len(e.allowed), func(i int) bool { - return e.allowed[i] >= r - }) >= 0 -} diff --git a/modules/context/api.go b/modules/context/api.go index 3f52c54d4..3f938948a 100644 --- a/modules/context/api.go +++ b/modules/context/api.go @@ -19,7 +19,6 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web/middleware" - auth_service "code.gitea.io/gitea/services/auth" ) // APIContext is a specific context for API service @@ -215,35 +214,6 @@ func (ctx *APIContext) CheckForOTP() { } } -// APIAuth converts auth_service.Auth as a middleware -func APIAuth(authMethod auth_service.Method) func(*APIContext) { - return func(ctx *APIContext) { - // Get user from session if logged in. - var err error - ctx.Doer, err = authMethod.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session) - if err != nil { - ctx.Error(http.StatusUnauthorized, "APIAuth", err) - return - } - - if ctx.Doer != nil { - if ctx.Locale.Language() != ctx.Doer.Language { - ctx.Locale = middleware.Locale(ctx.Resp, ctx.Req) - } - ctx.IsBasicAuth = ctx.Data["AuthedMethod"].(string) == auth_service.BasicMethodName - ctx.IsSigned = true - ctx.Data["IsSigned"] = ctx.IsSigned - ctx.Data["SignedUser"] = ctx.Doer - ctx.Data["SignedUserID"] = ctx.Doer.ID - ctx.Data["SignedUserName"] = ctx.Doer.Name - ctx.Data["IsAdmin"] = ctx.Doer.IsAdmin - } else { - ctx.Data["SignedUserID"] = int64(0) - ctx.Data["SignedUserName"] = "" - } - } -} - // APIContexter returns apicontext as middleware func APIContexter() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { diff --git a/modules/context/captcha.go b/modules/context/captcha.go index 735613504..07232e939 100644 --- a/modules/context/captcha.go +++ b/modules/context/captcha.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/modules/mcaptcha" "code.gitea.io/gitea/modules/recaptcha" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/turnstile" "gitea.com/go-chi/captcha" ) @@ -47,12 +48,14 @@ func SetCaptchaData(ctx *Context) { ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey ctx.Data["McaptchaSitekey"] = setting.Service.McaptchaSitekey ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL + ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey } const ( - gRecaptchaResponseField = "g-recaptcha-response" - hCaptchaResponseField = "h-captcha-response" - mCaptchaResponseField = "m-captcha-response" + gRecaptchaResponseField = "g-recaptcha-response" + hCaptchaResponseField = "h-captcha-response" + mCaptchaResponseField = "m-captcha-response" + cfTurnstileResponseField = "cf-turnstile-response" ) // VerifyCaptcha verifies Captcha data @@ -73,6 +76,8 @@ func VerifyCaptcha(ctx *Context, tpl base.TplName, form interface{}) { valid, err = hcaptcha.Verify(ctx, ctx.Req.Form.Get(hCaptchaResponseField)) case setting.MCaptcha: valid, err = mcaptcha.Verify(ctx, ctx.Req.Form.Get(mCaptchaResponseField)) + case setting.CfTurnstile: + valid, err = turnstile.Verify(ctx, ctx.Req.Form.Get(cfTurnstileResponseField)) default: ctx.ServerError("Unknown Captcha Type", fmt.Errorf("Unknown Captcha Type: %s", setting.Service.CaptchaType)) return diff --git a/modules/context/context.go b/modules/context/context.go index 84f40ce06..a2088217f 100644 --- a/modules/context/context.go +++ b/modules/context/context.go @@ -36,7 +36,6 @@ import ( "code.gitea.io/gitea/modules/typesniffer" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web/middleware" - "code.gitea.io/gitea/services/auth" "gitea.com/go-chi/cache" "gitea.com/go-chi/session" @@ -659,37 +658,6 @@ func getCsrfOpts() CsrfOptions { } } -// Auth converts auth.Auth as a middleware -func Auth(authMethod auth.Method) func(*Context) { - return func(ctx *Context) { - var err error - ctx.Doer, err = authMethod.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session) - if err != nil { - log.Error("Failed to verify user %v: %v", ctx.Req.RemoteAddr, err) - ctx.Error(http.StatusUnauthorized, "Verify") - return - } - if ctx.Doer != nil { - if ctx.Locale.Language() != ctx.Doer.Language { - ctx.Locale = middleware.Locale(ctx.Resp, ctx.Req) - } - ctx.IsBasicAuth = ctx.Data["AuthedMethod"].(string) == auth.BasicMethodName - ctx.IsSigned = true - ctx.Data["IsSigned"] = ctx.IsSigned - ctx.Data["SignedUser"] = ctx.Doer - ctx.Data["SignedUserID"] = ctx.Doer.ID - ctx.Data["SignedUserName"] = ctx.Doer.Name - ctx.Data["IsAdmin"] = ctx.Doer.IsAdmin - } else { - ctx.Data["SignedUserID"] = int64(0) - ctx.Data["SignedUserName"] = "" - - // ensure the session uid is deleted - _ = ctx.Session.Delete("uid") - } - } -} - // Contexter initializes a classic context for a request. func Contexter(ctx context.Context) func(next http.Handler) http.Handler { _, rnd := templates.HTMLRenderer(ctx) diff --git a/modules/context/org.go b/modules/context/org.go index ff3a5ae7e..0add7f2c0 100644 --- a/modules/context/org.go +++ b/modules/context/org.go @@ -80,7 +80,7 @@ func HandleOrgAssignment(ctx *Context, args ...bool) { orgName := ctx.Params(":org") var err error - ctx.Org.Organization, err = organization.GetOrgByName(orgName) + ctx.Org.Organization, err = organization.GetOrgByName(ctx, orgName) if err != nil { if organization.IsErrOrgNotExist(err) { redirectUserID, err := user_model.LookupUserRedirect(orgName) diff --git a/modules/git/blame.go b/modules/git/blame.go index 3b6e4c95d..ec88accb1 100644 --- a/modules/git/blame.go +++ b/modules/git/blame.go @@ -20,11 +20,12 @@ type BlamePart struct { // BlameReader returns part of file blame one by one type BlameReader struct { - cmd *Command - output io.WriteCloser - reader io.ReadCloser - done chan error - lastSha *string + cmd *Command + output io.WriteCloser + reader io.ReadCloser + bufferedReader *bufio.Reader + done chan error + lastSha *string } var shaLineRegex = regexp.MustCompile("^([a-z0-9]{40})") @@ -33,8 +34,6 @@ var shaLineRegex = regexp.MustCompile("^([a-z0-9]{40})") func (r *BlameReader) NextPart() (*BlamePart, error) { var blamePart *BlamePart - reader := bufio.NewReader(r.reader) - if r.lastSha != nil { blamePart = &BlamePart{*r.lastSha, make([]string, 0)} } @@ -44,7 +43,7 @@ func (r *BlameReader) NextPart() (*BlamePart, error) { var err error for err != io.EOF { - line, isPrefix, err = reader.ReadLine() + line, isPrefix, err = r.bufferedReader.ReadLine() if err != nil && err != io.EOF { return blamePart, err } @@ -66,7 +65,7 @@ func (r *BlameReader) NextPart() (*BlamePart, error) { r.lastSha = &sha1 // need to munch to end of line... for isPrefix { - _, isPrefix, err = reader.ReadLine() + _, isPrefix, err = r.bufferedReader.ReadLine() if err != nil && err != io.EOF { return blamePart, err } @@ -81,7 +80,7 @@ func (r *BlameReader) NextPart() (*BlamePart, error) { // need to munch to end of line... for isPrefix { - _, isPrefix, err = reader.ReadLine() + _, isPrefix, err = r.bufferedReader.ReadLine() if err != nil && err != io.EOF { return blamePart, err } @@ -96,6 +95,7 @@ func (r *BlameReader) NextPart() (*BlamePart, error) { // Close BlameReader - don't run NextPart after invoking that func (r *BlameReader) Close() error { err := <-r.done + r.bufferedReader = nil _ = r.reader.Close() _ = r.output.Close() return err @@ -126,10 +126,13 @@ func CreateBlameReader(ctx context.Context, repoPath, commitID, file string) (*B done <- err }(cmd, repoPath, stdout, done) + bufferedReader := bufio.NewReader(reader) + return &BlameReader{ - cmd: cmd, - output: stdout, - reader: reader, - done: done, + cmd: cmd, + output: stdout, + reader: reader, + bufferedReader: bufferedReader, + done: done, }, nil } diff --git a/modules/git/blame_test.go b/modules/git/blame_test.go index a2c8fe8e7..1c0cd5c4a 100644 --- a/modules/git/blame_test.go +++ b/modules/git/blame_test.go @@ -28,7 +28,7 @@ func TestReadingBlameOutput(t *testing.T) { }, { "f32b0a9dfd09a60f616f29158f772cedd89942d2", - []string{}, + []string{"", "Do not make any changes to this repo it is used for unit testing"}, }, } diff --git a/modules/git/repo.go b/modules/git/repo.go index e77a3a6ad..233f7f20c 100644 --- a/modules/git/repo.go +++ b/modules/git/repo.go @@ -163,10 +163,8 @@ func CloneWithArgs(ctx context.Context, args TrustedCmdArgs, from, to string, op envs := os.Environ() u, err := url.Parse(from) - if err == nil && (strings.EqualFold(u.Scheme, "http") || strings.EqualFold(u.Scheme, "https")) { - if proxy.Match(u.Host) { - envs = append(envs, fmt.Sprintf("https_proxy=%s", proxy.GetProxyURL())) - } + if err == nil { + envs = proxy.EnvWithProxy(u) } stderr := new(bytes.Buffer) diff --git a/modules/git/repo_attribute.go b/modules/git/repo_attribute.go index e7d5fb680..2b34f117f 100644 --- a/modules/git/repo_attribute.go +++ b/modules/git/repo_attribute.go @@ -135,8 +135,7 @@ func (c *CheckAttributeReader) Init(ctx context.Context) error { c.env = append(c.env, "GIT_FLUSH=1") - // The empty "--" comes from #16773 , and it seems unnecessary because nothing else would be added later. - c.cmd.AddDynamicArguments(c.Attributes...).AddArguments("--") + c.cmd.AddDynamicArguments(c.Attributes...) var err error diff --git a/modules/metrics/collector.go b/modules/metrics/collector.go index 17f8dd133..94699c161 100755 --- a/modules/metrics/collector.go +++ b/modules/metrics/collector.go @@ -4,7 +4,10 @@ package metrics import ( + "runtime" + activities_model "code.gitea.io/gitea/models/activities" + "code.gitea.io/gitea/modules/setting" "github.com/prometheus/client_golang/prometheus" ) @@ -17,6 +20,7 @@ type Collector struct { Accesses *prometheus.Desc Actions *prometheus.Desc Attachments *prometheus.Desc + BuildInfo *prometheus.Desc Comments *prometheus.Desc Follows *prometheus.Desc HookTasks *prometheus.Desc @@ -62,6 +66,16 @@ func NewCollector() Collector { "Number of Attachments", nil, nil, ), + BuildInfo: prometheus.NewDesc( + namespace+"build_info", + "Build information", + []string{ + "goarch", + "goos", + "goversion", + "version", + }, nil, + ), Comments: prometheus.NewDesc( namespace+"comments", "Number of Comments", @@ -195,6 +209,7 @@ func (c Collector) Describe(ch chan<- *prometheus.Desc) { ch <- c.Accesses ch <- c.Actions ch <- c.Attachments + ch <- c.BuildInfo ch <- c.Comments ch <- c.Follows ch <- c.HookTasks @@ -242,6 +257,15 @@ func (c Collector) Collect(ch chan<- prometheus.Metric) { float64(stats.Counter.Attachment), ) ch <- prometheus.MustNewConstMetric( + c.BuildInfo, + prometheus.GaugeValue, + 1, + runtime.GOARCH, + runtime.GOOS, + runtime.Version(), + setting.AppVer, + ) + ch <- prometheus.MustNewConstMetric( c.Comments, prometheus.GaugeValue, float64(stats.Counter.Comment), diff --git a/modules/migration/comment.go b/modules/migration/comment.go index f994e972e..92ce30e30 100644 --- a/modules/migration/comment.go +++ b/modules/migration/comment.go @@ -8,8 +8,7 @@ import "time" // Commentable can be commented upon type Commentable interface { - GetLocalIndex() int64 - GetForeignIndex() int64 + Reviewable GetContext() DownloaderContext } diff --git a/modules/migration/issue.go b/modules/migration/issue.go index 7cb9f84b0..3d1d1b4e0 100644 --- a/modules/migration/issue.go +++ b/modules/migration/issue.go @@ -34,6 +34,15 @@ func (issue *Issue) GetExternalName() string { return issue.PosterName } // GetExternalID ExternalUserMigrated interface func (issue *Issue) GetExternalID() int64 { return issue.PosterID } -func (issue *Issue) GetLocalIndex() int64 { return issue.Number } -func (issue *Issue) GetForeignIndex() int64 { return issue.ForeignIndex } +func (issue *Issue) GetLocalIndex() int64 { return issue.Number } + +func (issue *Issue) GetForeignIndex() int64 { + // see the comment of Reviewable.GetForeignIndex + // if there is no ForeignIndex, then use LocalIndex + if issue.ForeignIndex == 0 { + return issue.Number + } + return issue.ForeignIndex +} + func (issue *Issue) GetContext() DownloaderContext { return issue.Context } diff --git a/modules/migration/review.go b/modules/migration/review.go index a420c130c..79e821b2e 100644 --- a/modules/migration/review.go +++ b/modules/migration/review.go @@ -8,6 +8,16 @@ import "time" // Reviewable can be reviewed type Reviewable interface { GetLocalIndex() int64 + + // GetForeignIndex presents the foreign index, which could be misused: + // For example, if there are 2 Gitea sites: site-A exports a dataset, then site-B imports it: + // * if site-A exports files by using its LocalIndex + // * from site-A's view, LocalIndex is site-A's IssueIndex while ForeignIndex is site-B's IssueIndex + // * but from site-B's view, LocalIndex is site-B's IssueIndex while ForeignIndex is site-A's IssueIndex + // + // So the exporting/importing must be paired, but the meaning of them looks confusing then: + // * either site-A and site-B both use LocalIndex during dumping/restoring + // * or site-A and site-B both use ForeignIndex GetForeignIndex() int64 } @@ -37,7 +47,7 @@ type Review struct { // GetExternalName ExternalUserMigrated interface func (r *Review) GetExternalName() string { return r.ReviewerName } -// ExternalID ExternalUserMigrated interface +// GetExternalID ExternalUserMigrated interface func (r *Review) GetExternalID() int64 { return r.ReviewerID } // ReviewComment represents a review comment diff --git a/modules/packages/cargo/parser.go b/modules/packages/cargo/parser.go new file mode 100644 index 000000000..36cd44df8 --- /dev/null +++ b/modules/packages/cargo/parser.go @@ -0,0 +1,169 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cargo + +import ( + "encoding/binary" + "errors" + "io" + "regexp" + + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/validation" + + "github.com/hashicorp/go-version" +) + +const PropertyYanked = "cargo.yanked" + +var ( + ErrInvalidName = errors.New("package name is invalid") + ErrInvalidVersion = errors.New("package version is invalid") +) + +// Package represents a Cargo package +type Package struct { + Name string + Version string + Metadata *Metadata + Content io.Reader + ContentSize int64 +} + +// Metadata represents the metadata of a Cargo package +type Metadata struct { + Dependencies []*Dependency `json:"dependencies,omitempty"` + Features map[string][]string `json:"features,omitempty"` + Authors []string `json:"authors,omitempty"` + Description string `json:"description,omitempty"` + DocumentationURL string `json:"documentation_url,omitempty"` + ProjectURL string `json:"project_url,omitempty"` + Readme string `json:"readme,omitempty"` + Keywords []string `json:"keywords,omitempty"` + Categories []string `json:"categories,omitempty"` + License string `json:"license,omitempty"` + RepositoryURL string `json:"repository_url,omitempty"` + Links string `json:"links,omitempty"` +} + +type Dependency struct { + Name string `json:"name"` + Req string `json:"req"` + Features []string `json:"features"` + Optional bool `json:"optional"` + DefaultFeatures bool `json:"default_features"` + Target *string `json:"target"` + Kind string `json:"kind"` + Registry *string `json:"registry"` + Package *string `json:"package"` +} + +var nameMatch = regexp.MustCompile(`\A[a-zA-Z][a-zA-Z0-9-_]{0,63}\z`) + +// ParsePackage reads the metadata and content of a package +func ParsePackage(r io.Reader) (*Package, error) { + var size uint32 + if err := binary.Read(r, binary.LittleEndian, &size); err != nil { + return nil, err + } + + p, err := parsePackage(io.LimitReader(r, int64(size))) + if err != nil { + return nil, err + } + + if err := binary.Read(r, binary.LittleEndian, &size); err != nil { + return nil, err + } + + p.Content = io.LimitReader(r, int64(size)) + p.ContentSize = int64(size) + + return p, nil +} + +func parsePackage(r io.Reader) (*Package, error) { + var meta struct { + Name string `json:"name"` + Vers string `json:"vers"` + Deps []struct { + Name string `json:"name"` + VersionReq string `json:"version_req"` + Features []string `json:"features"` + Optional bool `json:"optional"` + DefaultFeatures bool `json:"default_features"` + Target *string `json:"target"` + Kind string `json:"kind"` + Registry *string `json:"registry"` + ExplicitNameInToml string `json:"explicit_name_in_toml"` + } `json:"deps"` + Features map[string][]string `json:"features"` + Authors []string `json:"authors"` + Description string `json:"description"` + Documentation string `json:"documentation"` + Homepage string `json:"homepage"` + Readme string `json:"readme"` + ReadmeFile string `json:"readme_file"` + Keywords []string `json:"keywords"` + Categories []string `json:"categories"` + License string `json:"license"` + LicenseFile string `json:"license_file"` + Repository string `json:"repository"` + Links string `json:"links"` + } + if err := json.NewDecoder(r).Decode(&meta); err != nil { + return nil, err + } + + if !nameMatch.MatchString(meta.Name) { + return nil, ErrInvalidName + } + + if _, err := version.NewSemver(meta.Vers); err != nil { + return nil, ErrInvalidVersion + } + + if !validation.IsValidURL(meta.Homepage) { + meta.Homepage = "" + } + if !validation.IsValidURL(meta.Documentation) { + meta.Documentation = "" + } + if !validation.IsValidURL(meta.Repository) { + meta.Repository = "" + } + + dependencies := make([]*Dependency, 0, len(meta.Deps)) + for _, dep := range meta.Deps { + dependencies = append(dependencies, &Dependency{ + Name: dep.Name, + Req: dep.VersionReq, + Features: dep.Features, + Optional: dep.Optional, + DefaultFeatures: dep.DefaultFeatures, + Target: dep.Target, + Kind: dep.Kind, + Registry: dep.Registry, + }) + } + + return &Package{ + Name: meta.Name, + Version: meta.Vers, + Metadata: &Metadata{ + Dependencies: dependencies, + Features: meta.Features, + Authors: meta.Authors, + Description: meta.Description, + DocumentationURL: meta.Documentation, + ProjectURL: meta.Homepage, + Readme: meta.Readme, + Keywords: meta.Keywords, + Categories: meta.Categories, + License: meta.License, + RepositoryURL: meta.Repository, + Links: meta.Links, + }, + }, nil +} diff --git a/modules/packages/cargo/parser_test.go b/modules/packages/cargo/parser_test.go new file mode 100644 index 000000000..2230a5b49 --- /dev/null +++ b/modules/packages/cargo/parser_test.go @@ -0,0 +1,86 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cargo + +import ( + "bytes" + "encoding/binary" + "io" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +const ( + description = "Package Description" + author = "KN4CK3R" + homepage = "https://gitea.io/" + license = "MIT" +) + +func TestParsePackage(t *testing.T) { + createPackage := func(name, version string) io.Reader { + metadata := `{ + "name":"` + name + `", + "vers":"` + version + `", + "description":"` + description + `", + "authors": ["` + author + `"], + "deps":[ + { + "name":"dep", + "version_req":"1.0" + } + ], + "homepage":"` + homepage + `", + "license":"` + license + `" +}` + + var buf bytes.Buffer + binary.Write(&buf, binary.LittleEndian, uint32(len(metadata))) + buf.WriteString(metadata) + binary.Write(&buf, binary.LittleEndian, uint32(4)) + buf.WriteString("test") + return &buf + } + + t.Run("InvalidName", func(t *testing.T) { + for _, name := range []string{"", "0test", "-test", "_test", strings.Repeat("a", 65)} { + data := createPackage(name, "1.0.0") + + cp, err := ParsePackage(data) + assert.Nil(t, cp) + assert.ErrorIs(t, err, ErrInvalidName) + } + }) + + t.Run("InvalidVersion", func(t *testing.T) { + for _, version := range []string{"", "1.", "-1.0", "1.0.0/1"} { + data := createPackage("test", version) + + cp, err := ParsePackage(data) + assert.Nil(t, cp) + assert.ErrorIs(t, err, ErrInvalidVersion) + } + }) + + t.Run("Valid", func(t *testing.T) { + data := createPackage("test", "1.0.0") + + cp, err := ParsePackage(data) + assert.NotNil(t, cp) + assert.NoError(t, err) + + assert.Equal(t, "test", cp.Name) + assert.Equal(t, "1.0.0", cp.Version) + assert.Equal(t, description, cp.Metadata.Description) + assert.Equal(t, []string{author}, cp.Metadata.Authors) + assert.Len(t, cp.Metadata.Dependencies, 1) + assert.Equal(t, "dep", cp.Metadata.Dependencies[0].Name) + assert.Equal(t, homepage, cp.Metadata.ProjectURL) + assert.Equal(t, license, cp.Metadata.License) + content, _ := io.ReadAll(cp.Content) + assert.Equal(t, "test", string(content)) + }) +} diff --git a/modules/packages/chef/metadata.go b/modules/packages/chef/metadata.go new file mode 100644 index 000000000..a1c91870c --- /dev/null +++ b/modules/packages/chef/metadata.go @@ -0,0 +1,134 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package chef + +import ( + "archive/tar" + "compress/gzip" + "io" + "regexp" + "strings" + + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/validation" +) + +const ( + KeyBits = 4096 + SettingPublicPem = "chef.public_pem" +) + +var ( + ErrMissingMetadataFile = util.NewInvalidArgumentErrorf("metadata.json file is missing") + ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid") + ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid") + + namePattern = regexp.MustCompile(`\A\S+\z`) + versionPattern = regexp.MustCompile(`\A\d+\.\d+(?:\.\d+)?\z`) +) + +// Package represents a Chef package +type Package struct { + Name string + Version string + Metadata *Metadata +} + +// Metadata represents the metadata of a Chef package +type Metadata struct { + Description string `json:"description,omitempty"` + LongDescription string `json:"long_description,omitempty"` + Author string `json:"author,omitempty"` + License string `json:"license,omitempty"` + RepositoryURL string `json:"repository_url,omitempty"` + Dependencies map[string]string `json:"dependencies,omitempty"` +} + +type chefMetadata struct { + Name string `json:"name"` + Description string `json:"description"` + LongDescription string `json:"long_description"` + Maintainer string `json:"maintainer"` + MaintainerEmail string `json:"maintainer_email"` + License string `json:"license"` + Platforms map[string]string `json:"platforms"` + Dependencies map[string]string `json:"dependencies"` + Providing map[string]string `json:"providing"` + Recipes map[string]string `json:"recipes"` + Version string `json:"version"` + SourceURL string `json:"source_url"` + IssuesURL string `json:"issues_url"` + Privacy bool `json:"privacy"` + ChefVersions [][]string `json:"chef_versions"` + Gems [][]string `json:"gems"` + EagerLoadLibraries bool `json:"eager_load_libraries"` +} + +// ParsePackage parses the Chef package file +func ParsePackage(r io.Reader) (*Package, error) { + gzr, err := gzip.NewReader(r) + if err != nil { + return nil, err + } + defer gzr.Close() + + tr := tar.NewReader(gzr) + for { + hd, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + + if hd.Typeflag != tar.TypeReg { + continue + } + + if strings.Count(hd.Name, "/") != 1 { + continue + } + + if hd.FileInfo().Name() == "metadata.json" { + return ParseChefMetadata(tr) + } + } + + return nil, ErrMissingMetadataFile +} + +// ParseChefMetadata parses a metadata.json file to retrieve the metadata of a Chef package +func ParseChefMetadata(r io.Reader) (*Package, error) { + var cm chefMetadata + if err := json.NewDecoder(r).Decode(&cm); err != nil { + return nil, err + } + + if !namePattern.MatchString(cm.Name) { + return nil, ErrInvalidName + } + + if !versionPattern.MatchString(cm.Version) { + return nil, ErrInvalidVersion + } + + if !validation.IsValidURL(cm.SourceURL) { + cm.SourceURL = "" + } + + return &Package{ + Name: cm.Name, + Version: cm.Version, + Metadata: &Metadata{ + Description: cm.Description, + LongDescription: cm.LongDescription, + Author: cm.Maintainer, + License: cm.License, + RepositoryURL: cm.SourceURL, + Dependencies: cm.Dependencies, + }, + }, nil +} diff --git a/modules/packages/chef/metadata_test.go b/modules/packages/chef/metadata_test.go new file mode 100644 index 000000000..6def4162a --- /dev/null +++ b/modules/packages/chef/metadata_test.go @@ -0,0 +1,92 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package chef + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +const ( + packageName = "gitea" + packageVersion = "1.0.1" + packageAuthor = "KN4CK3R" + packageDescription = "Package Description" + packageRepositoryURL = "https://gitea.io/gitea/gitea" +) + +func TestParsePackage(t *testing.T) { + t.Run("MissingMetadataFile", func(t *testing.T) { + var buf bytes.Buffer + zw := gzip.NewWriter(&buf) + tw := tar.NewWriter(zw) + tw.Close() + zw.Close() + + p, err := ParsePackage(&buf) + assert.Nil(t, p) + assert.ErrorIs(t, err, ErrMissingMetadataFile) + }) + + t.Run("Valid", func(t *testing.T) { + var buf bytes.Buffer + zw := gzip.NewWriter(&buf) + tw := tar.NewWriter(zw) + + content := `{"name":"` + packageName + `","version":"` + packageVersion + `"}` + + hdr := &tar.Header{ + Name: packageName + "/metadata.json", + Mode: 0o600, + Size: int64(len(content)), + } + tw.WriteHeader(hdr) + tw.Write([]byte(content)) + + tw.Close() + zw.Close() + + p, err := ParsePackage(&buf) + assert.NoError(t, err) + assert.NotNil(t, p) + assert.Equal(t, packageName, p.Name) + assert.Equal(t, packageVersion, p.Version) + assert.NotNil(t, p.Metadata) + }) +} + +func TestParseChefMetadata(t *testing.T) { + t.Run("InvalidName", func(t *testing.T) { + for _, name := range []string{" test", "test "} { + p, err := ParseChefMetadata(strings.NewReader(`{"name":"` + name + `","version":"1.0.0"}`)) + assert.Nil(t, p) + assert.ErrorIs(t, err, ErrInvalidName) + } + }) + + t.Run("InvalidVersion", func(t *testing.T) { + for _, version := range []string{"1", "1.2.3.4", "1.0.0 "} { + p, err := ParseChefMetadata(strings.NewReader(`{"name":"test","version":"` + version + `"}`)) + assert.Nil(t, p) + assert.ErrorIs(t, err, ErrInvalidVersion) + } + }) + + t.Run("Valid", func(t *testing.T) { + p, err := ParseChefMetadata(strings.NewReader(`{"name":"` + packageName + `","version":"` + packageVersion + `","description":"` + packageDescription + `","maintainer":"` + packageAuthor + `","source_url":"` + packageRepositoryURL + `"}`)) + assert.NotNil(t, p) + assert.NoError(t, err) + + assert.Equal(t, packageName, p.Name) + assert.Equal(t, packageVersion, p.Version) + assert.Equal(t, packageDescription, p.Metadata.Description) + assert.Equal(t, packageAuthor, p.Metadata.Author) + assert.Equal(t, packageRepositoryURL, p.Metadata.RepositoryURL) + }) +} diff --git a/modules/packages/container/metadata.go b/modules/packages/container/metadata.go index c3946f38f..6f62ab6a5 100644 --- a/modules/packages/container/metadata.go +++ b/modules/packages/container/metadata.go @@ -10,8 +10,9 @@ import ( "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/packages/container/helm" - "code.gitea.io/gitea/modules/packages/container/oci" "code.gitea.io/gitea/modules/validation" + + oci "github.com/opencontainers/image-spec/specs-go/v1" ) const ( @@ -65,8 +66,8 @@ type Metadata struct { } // ParseImageConfig parses the metadata of an image config -func ParseImageConfig(mediaType oci.MediaType, r io.Reader) (*Metadata, error) { - if strings.EqualFold(string(mediaType), helm.ConfigMediaType) { +func ParseImageConfig(mt string, r io.Reader) (*Metadata, error) { + if strings.EqualFold(mt, helm.ConfigMediaType) { return parseHelmConfig(r) } diff --git a/modules/packages/container/metadata_test.go b/modules/packages/container/metadata_test.go index f9ee478d6..5d8d3abfa 100644 --- a/modules/packages/container/metadata_test.go +++ b/modules/packages/container/metadata_test.go @@ -8,8 +8,8 @@ import ( "testing" "code.gitea.io/gitea/modules/packages/container/helm" - "code.gitea.io/gitea/modules/packages/container/oci" + oci "github.com/opencontainers/image-spec/specs-go/v1" "github.com/stretchr/testify/assert" ) @@ -23,7 +23,7 @@ func TestParseImageConfig(t *testing.T) { configOCI := `{"config": {"labels": {"` + labelAuthors + `": "` + author + `", "` + labelLicenses + `": "` + license + `", "` + labelURL + `": "` + projectURL + `", "` + labelSource + `": "` + repositoryURL + `", "` + labelDocumentation + `": "` + documentationURL + `", "` + labelDescription + `": "` + description + `"}}, "history": [{"created_by": "do it 1"}, {"created_by": "dummy #(nop) do it 2"}]}` - metadata, err := ParseImageConfig(oci.MediaType(oci.MediaTypeImageManifest), strings.NewReader(configOCI)) + metadata, err := ParseImageConfig(oci.MediaTypeImageManifest, strings.NewReader(configOCI)) assert.NoError(t, err) assert.Equal(t, TypeOCI, metadata.Type) @@ -50,7 +50,7 @@ func TestParseImageConfig(t *testing.T) { configHelm := `{"description":"` + description + `", "home": "` + projectURL + `", "sources": ["` + repositoryURL + `"], "maintainers":[{"name":"` + author + `"}]}` - metadata, err = ParseImageConfig(oci.MediaType(helm.ConfigMediaType), strings.NewReader(configHelm)) + metadata, err = ParseImageConfig(helm.ConfigMediaType, strings.NewReader(configHelm)) assert.NoError(t, err) assert.Equal(t, TypeHelm, metadata.Type) diff --git a/modules/packages/container/oci/digest.go b/modules/packages/container/oci/digest.go deleted file mode 100644 index dd9cc0095..000000000 --- a/modules/packages/container/oci/digest.go +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package oci - -import ( - "regexp" - "strings" -) - -var digestPattern = regexp.MustCompile(`\Asha256:[a-f0-9]{64}\z`) - -type Digest string - -// Validate checks if the digest has a valid SHA256 signature -func (d Digest) Validate() bool { - return digestPattern.MatchString(string(d)) -} - -func (d Digest) Hash() string { - p := strings.SplitN(string(d), ":", 2) - if len(p) != 2 { - return "" - } - return p[1] -} diff --git a/modules/packages/container/oci/mediatype.go b/modules/packages/container/oci/mediatype.go deleted file mode 100644 index f9c3907e1..000000000 --- a/modules/packages/container/oci/mediatype.go +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package oci - -import ( - "strings" -) - -const ( - MediaTypeImageManifest = "application/vnd.oci.image.manifest.v1+json" - MediaTypeImageIndex = "application/vnd.oci.image.index.v1+json" - MediaTypeDockerManifest = "application/vnd.docker.distribution.manifest.v2+json" - MediaTypeDockerManifestList = "application/vnd.docker.distribution.manifest.list.v2+json" -) - -type MediaType string - -// IsValid tests if the media type is in the OCI or Docker namespace -func (m MediaType) IsValid() bool { - s := string(m) - return strings.HasPrefix(s, "application/vnd.docker.") || strings.HasPrefix(s, "application/vnd.oci.") -} - -// IsImageManifest tests if the media type is an image manifest -func (m MediaType) IsImageManifest() bool { - s := string(m) - return strings.EqualFold(s, MediaTypeDockerManifest) || strings.EqualFold(s, MediaTypeImageManifest) -} - -// IsImageIndex tests if the media type is an image index -func (m MediaType) IsImageIndex() bool { - s := string(m) - return strings.EqualFold(s, MediaTypeDockerManifestList) || strings.EqualFold(s, MediaTypeImageIndex) -} diff --git a/modules/packages/container/oci/oci.go b/modules/packages/container/oci/oci.go deleted file mode 100644 index 570d2e92c..000000000 --- a/modules/packages/container/oci/oci.go +++ /dev/null @@ -1,190 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package oci - -import ( - "time" -) - -// https://github.com/opencontainers/image-spec/tree/main/specs-go/v1 - -// ImageConfig defines the execution parameters which should be used as a base when running a container using an image. -type ImageConfig struct { - // User defines the username or UID which the process in the container should run as. - User string `json:"User,omitempty"` - - // ExposedPorts a set of ports to expose from a container running this image. - ExposedPorts map[string]struct{} `json:"ExposedPorts,omitempty"` - - // Env is a list of environment variables to be used in a container. - Env []string `json:"Env,omitempty"` - - // Entrypoint defines a list of arguments to use as the command to execute when the container starts. - Entrypoint []string `json:"Entrypoint,omitempty"` - - // Cmd defines the default arguments to the entrypoint of the container. - Cmd []string `json:"Cmd,omitempty"` - - // Volumes is a set of directories describing where the process is likely write data specific to a container instance. - Volumes map[string]struct{} `json:"Volumes,omitempty"` - - // WorkingDir sets the current working directory of the entrypoint process in the container. - WorkingDir string `json:"WorkingDir,omitempty"` - - // Labels contains arbitrary metadata for the container. - Labels map[string]string `json:"Labels,omitempty"` - - // StopSignal contains the system call signal that will be sent to the container to exit. - StopSignal string `json:"StopSignal,omitempty"` -} - -// RootFS describes a layer content addresses -type RootFS struct { - // Type is the type of the rootfs. - Type string `json:"type"` - - // DiffIDs is an array of layer content hashes, in order from bottom-most to top-most. - DiffIDs []string `json:"diff_ids"` -} - -// History describes the history of a layer. -type History struct { - // Created is the combined date and time at which the layer was created, formatted as defined by RFC 3339, section 5.6. - Created *time.Time `json:"created,omitempty"` - - // CreatedBy is the command which created the layer. - CreatedBy string `json:"created_by,omitempty"` - - // Author is the author of the build point. - Author string `json:"author,omitempty"` - - // Comment is a custom message set when creating the layer. - Comment string `json:"comment,omitempty"` - - // EmptyLayer is used to mark if the history item created a filesystem diff. - EmptyLayer bool `json:"empty_layer,omitempty"` -} - -// Image is the JSON structure which describes some basic information about the image. -// This provides the `application/vnd.oci.image.config.v1+json` mediatype when marshalled to JSON. -type Image struct { - // Created is the combined date and time at which the image was created, formatted as defined by RFC 3339, section 5.6. - Created *time.Time `json:"created,omitempty"` - - // Author defines the name and/or email address of the person or entity which created and is responsible for maintaining the image. - Author string `json:"author,omitempty"` - - // Architecture is the CPU architecture which the binaries in this image are built to run on. - Architecture string `json:"architecture"` - - // Variant is the variant of the specified CPU architecture which image binaries are intended to run on. - Variant string `json:"variant,omitempty"` - - // OS is the name of the operating system which the image is built to run on. - OS string `json:"os"` - - // OSVersion is an optional field specifying the operating system - // version, for example on Windows `10.0.14393.1066`. - OSVersion string `json:"os.version,omitempty"` - - // OSFeatures is an optional field specifying an array of strings, - // each listing a required OS feature (for example on Windows `win32k`). - OSFeatures []string `json:"os.features,omitempty"` - - // Config defines the execution parameters which should be used as a base when running a container using the image. - Config ImageConfig `json:"config,omitempty"` - - // RootFS references the layer content addresses used by the image. - RootFS RootFS `json:"rootfs"` - - // History describes the history of each layer. - History []History `json:"history,omitempty"` -} - -// Descriptor describes the disposition of targeted content. -// This structure provides `application/vnd.oci.descriptor.v1+json` mediatype -// when marshalled to JSON. -type Descriptor struct { - // MediaType is the media type of the object this schema refers to. - MediaType MediaType `json:"mediaType,omitempty"` - - // Digest is the digest of the targeted content. - Digest Digest `json:"digest"` - - // Size specifies the size in bytes of the blob. - Size int64 `json:"size"` - - // URLs specifies a list of URLs from which this object MAY be downloaded - URLs []string `json:"urls,omitempty"` - - // Annotations contains arbitrary metadata relating to the targeted content. - Annotations map[string]string `json:"annotations,omitempty"` - - // Data is an embedding of the targeted content. This is encoded as a base64 - // string when marshalled to JSON (automatically, by encoding/json). If - // present, Data can be used directly to avoid fetching the targeted content. - Data []byte `json:"data,omitempty"` - - // Platform describes the platform which the image in the manifest runs on. - // - // This should only be used when referring to a manifest. - Platform *Platform `json:"platform,omitempty"` -} - -// Platform describes the platform which the image in the manifest runs on. -type Platform struct { - // Architecture field specifies the CPU architecture, for example - // `amd64` or `ppc64`. - Architecture string `json:"architecture"` - - // OS specifies the operating system, for example `linux` or `windows`. - OS string `json:"os"` - - // OSVersion is an optional field specifying the operating system - // version, for example on Windows `10.0.14393.1066`. - OSVersion string `json:"os.version,omitempty"` - - // OSFeatures is an optional field specifying an array of strings, - // each listing a required OS feature (for example on Windows `win32k`). - OSFeatures []string `json:"os.features,omitempty"` - - // Variant is an optional field specifying a variant of the CPU, for - // example `v7` to specify ARMv7 when architecture is `arm`. - Variant string `json:"variant,omitempty"` -} - -type SchemaMediaBase struct { - // SchemaVersion is the image manifest schema that this image follows - SchemaVersion int `json:"schemaVersion"` - - // MediaType specifies the type of this document data structure e.g. `application/vnd.oci.image.manifest.v1+json` - MediaType MediaType `json:"mediaType,omitempty"` -} - -// Manifest provides `application/vnd.oci.image.manifest.v1+json` mediatype structure when marshalled to JSON. -type Manifest struct { - SchemaMediaBase - - // Config references a configuration object for a container, by digest. - // The referenced configuration object is a JSON blob that the runtime uses to set up the container. - Config Descriptor `json:"config"` - - // Layers is an indexed list of layers referenced by the manifest. - Layers []Descriptor `json:"layers"` - - // Annotations contains arbitrary metadata for the image manifest. - Annotations map[string]string `json:"annotations,omitempty"` -} - -// Index references manifests for various platforms. -// This structure provides `application/vnd.oci.image.index.v1+json` mediatype when marshalled to JSON. -type Index struct { - SchemaMediaBase - - // Manifests references platform specific manifests. - Manifests []Descriptor `json:"manifests"` - - // Annotations contains arbitrary metadata for the image index. - Annotations map[string]string `json:"annotations,omitempty"` -} diff --git a/modules/packages/container/oci/reference.go b/modules/packages/container/oci/reference.go deleted file mode 100644 index 7ec399255..000000000 --- a/modules/packages/container/oci/reference.go +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package oci - -import ( - "regexp" -) - -var referencePattern = regexp.MustCompile(`\A[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}\z`) - -type Reference string - -func (r Reference) Validate() bool { - return referencePattern.MatchString(string(r)) -} diff --git a/modules/proxy/proxy.go b/modules/proxy/proxy.go index f0cd366c1..1a6bdad7f 100644 --- a/modules/proxy/proxy.go +++ b/modules/proxy/proxy.go @@ -7,6 +7,7 @@ import ( "net/http" "net/url" "os" + "strings" "sync" "code.gitea.io/gitea/modules/log" @@ -82,3 +83,16 @@ func Proxy() func(req *http.Request) (*url.URL, error) { return http.ProxyFromEnvironment(req) } } + +// EnvWithProxy returns os.Environ(), with a https_proxy env, if the given url +// needs to be proxied. +func EnvWithProxy(u *url.URL) []string { + envs := os.Environ() + if strings.EqualFold(u.Scheme, "http") || strings.EqualFold(u.Scheme, "https") { + if Match(u.Host) { + envs = append(envs, "https_proxy="+GetProxyURL()) + } + } + + return envs +} diff --git a/modules/repository/create.go b/modules/repository/create.go index 7bcda0fe4..b9a72ad57 100644 --- a/modules/repository/create.go +++ b/modules/repository/create.go @@ -211,6 +211,7 @@ func CreateRepository(doer, u *user_model.User, opts CreateRepoOptions) (*repo_m IsEmpty: !opts.AutoInit, TrustModel: opts.TrustModel, IsMirror: opts.IsMirror, + DefaultBranch: opts.DefaultBranch, } var rollbackRepo *repo_model.Repository diff --git a/modules/repository/create_test.go b/modules/repository/create_test.go index 293071bdc..e620422bc 100644 --- a/modules/repository/create_test.go +++ b/modules/repository/create_test.go @@ -49,7 +49,7 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) { assert.NoError(t, organization.CreateOrganization(org, user), "CreateOrganization") // Check Owner team. - ownerTeam, err := org.GetOwnerTeam() + ownerTeam, err := org.GetOwnerTeam(db.DefaultContext) assert.NoError(t, err, "GetOwnerTeam") assert.True(t, ownerTeam.IncludesAllRepositories, "Owner team includes all repositories") @@ -63,7 +63,7 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) { } } // Get fresh copy of Owner team after creating repos. - ownerTeam, err = org.GetOwnerTeam() + ownerTeam, err = org.GetOwnerTeam(db.DefaultContext) assert.NoError(t, err, "GetOwnerTeam") // Create teams and check repositories. diff --git a/modules/repository/repo.go b/modules/repository/repo.go index d1a70e7c1..c03e46999 100644 --- a/modules/repository/repo.go +++ b/modules/repository/repo.go @@ -57,7 +57,7 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User, repoPath := repo_model.RepoPath(u.Name, opts.RepoName) if u.IsOrganization() { - t, err := organization.OrgFromUser(u).GetOwnerTeam() + t, err := organization.OrgFromUser(u).GetOwnerTeam(ctx) if err != nil { return nil, err } diff --git a/modules/setting/packages.go b/modules/setting/packages.go index d0cd80aa0..84da4eb53 100644 --- a/modules/setting/packages.go +++ b/modules/setting/packages.go @@ -25,6 +25,8 @@ var ( LimitTotalOwnerCount int64 LimitTotalOwnerSize int64 + LimitSizeCargo int64 + LimitSizeChef int64 LimitSizeComposer int64 LimitSizeConan int64 LimitSizeConda int64 @@ -65,6 +67,8 @@ func newPackages() { } Packages.LimitTotalOwnerSize = mustBytes(sec, "LIMIT_TOTAL_OWNER_SIZE") + Packages.LimitSizeCargo = mustBytes(sec, "LIMIT_SIZE_CARGO") + Packages.LimitSizeChef = mustBytes(sec, "LIMIT_SIZE_CHEF") Packages.LimitSizeComposer = mustBytes(sec, "LIMIT_SIZE_COMPOSER") Packages.LimitSizeConan = mustBytes(sec, "LIMIT_SIZE_CONAN") Packages.LimitSizeConda = mustBytes(sec, "LIMIT_SIZE_CONDA") diff --git a/modules/setting/service.go b/modules/setting/service.go index 7b4bfc5c7..1d33ac6bc 100644 --- a/modules/setting/service.go +++ b/modules/setting/service.go @@ -46,6 +46,8 @@ var Service = struct { RecaptchaSecret string RecaptchaSitekey string RecaptchaURL string + CfTurnstileSecret string + CfTurnstileSitekey string HcaptchaSecret string HcaptchaSitekey string McaptchaSecret string @@ -137,6 +139,8 @@ func newService() { Service.RecaptchaSecret = sec.Key("RECAPTCHA_SECRET").MustString("") Service.RecaptchaSitekey = sec.Key("RECAPTCHA_SITEKEY").MustString("") Service.RecaptchaURL = sec.Key("RECAPTCHA_URL").MustString("https://www.google.com/recaptcha/") + Service.CfTurnstileSecret = sec.Key("CF_TURNSTILE_SECRET").MustString("") + Service.CfTurnstileSitekey = sec.Key("CF_TURNSTILE_SITEKEY").MustString("") Service.HcaptchaSecret = sec.Key("HCAPTCHA_SECRET").MustString("") Service.HcaptchaSitekey = sec.Key("HCAPTCHA_SITEKEY").MustString("") Service.McaptchaURL = sec.Key("MCAPTCHA_URL").MustString("https://demo.mcaptcha.org/") diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 23cd90553..a68a46f7a 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -61,6 +61,7 @@ const ( ReCaptcha = "recaptcha" HCaptcha = "hcaptcha" MCaptcha = "mcaptcha" + CfTurnstile = "cfturnstile" ) // settings diff --git a/modules/structs/repo.go b/modules/structs/repo.go index 16f3d9dd2..ee4bec4df 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -63,6 +63,7 @@ type Repository struct { Language string `json:"language"` LanguagesURL string `json:"languages_url"` HTMLURL string `json:"html_url"` + Link string `json:"link"` SSHURL string `json:"ssh_url"` CloneURL string `json:"clone_url"` OriginalURL string `json:"original_url"` diff --git a/modules/structs/org_type.go b/modules/structs/visible_type.go index 69f323fa9..b5ff353b8 100644 --- a/modules/structs/org_type.go +++ b/modules/structs/visible_type.go @@ -3,7 +3,7 @@ package structs -// VisibleType defines the visibility (Organization only) +// VisibleType defines the visibility of user and org type VisibleType int const ( @@ -13,11 +13,11 @@ const ( // VisibleTypeLimited Visible for every connected user VisibleTypeLimited - // VisibleTypePrivate Visible only for organization's members + // VisibleTypePrivate Visible only for self or admin user VisibleTypePrivate ) -// VisibilityModes is a map of org Visibility types +// VisibilityModes is a map of Visibility types var VisibilityModes = map[string]VisibleType{ "public": VisibleTypePublic, "limited": VisibleTypeLimited, diff --git a/modules/templates/helper.go b/modules/templates/helper.go index a390d9459..7afc3aa59 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -72,6 +72,10 @@ func NewFuncMap() []template.FuncMap { return setting.StaticURLPrefix + "/assets" }, "AppUrl": func() string { + // The usage of AppUrl should be avoided as much as possible, + // because the AppURL(ROOT_URL) may not match user's visiting site and the ROOT_URL in app.ini may be incorrect. + // And it's difficult for Gitea to guess absolute URL correctly with zero configuration, + // because Gitea doesn't know whether the scheme is HTTP or HTTPS unless the reverse proxy could tell Gitea. return setting.AppURL }, "AppVer": func() string { diff --git a/modules/turnstile/turnstile.go b/modules/turnstile/turnstile.go new file mode 100644 index 000000000..38d023344 --- /dev/null +++ b/modules/turnstile/turnstile.go @@ -0,0 +1,92 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package turnstile + +import ( + "context" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/setting" +) + +// Response is the structure of JSON returned from API +type Response struct { + Success bool `json:"success"` + ChallengeTS string `json:"challenge_ts"` + Hostname string `json:"hostname"` + ErrorCodes []ErrorCode `json:"error-codes"` + Action string `json:"login"` + Cdata string `json:"cdata"` +} + +// Verify calls Cloudflare Turnstile API to verify token +func Verify(ctx context.Context, response string) (bool, error) { + // Cloudflare turnstile official access instruction address: https://developers.cloudflare.com/turnstile/get-started/server-side-validation/ + post := url.Values{ + "secret": {setting.Service.CfTurnstileSecret}, + "response": {response}, + } + // Basically a copy of http.PostForm, but with a context + req, err := http.NewRequestWithContext(ctx, http.MethodPost, + "https://challenges.cloudflare.com/turnstile/v0/siteverify", strings.NewReader(post.Encode())) + if err != nil { + return false, fmt.Errorf("Failed to create CAPTCHA request: %w", err) + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return false, fmt.Errorf("Failed to send CAPTCHA response: %w", err) + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return false, fmt.Errorf("Failed to read CAPTCHA response: %w", err) + } + + var jsonResponse Response + if err := json.Unmarshal(body, &jsonResponse); err != nil { + return false, fmt.Errorf("Failed to parse CAPTCHA response: %w", err) + } + + var respErr error + if len(jsonResponse.ErrorCodes) > 0 { + respErr = jsonResponse.ErrorCodes[0] + } + return jsonResponse.Success, respErr +} + +// ErrorCode is a reCaptcha error +type ErrorCode string + +// String fulfills the Stringer interface +func (e ErrorCode) String() string { + switch e { + case "missing-input-secret": + return "The secret parameter was not passed." + case "invalid-input-secret": + return "The secret parameter was invalid or did not exist." + case "missing-input-response": + return "The response parameter was not passed." + case "invalid-input-response": + return "The response parameter is invalid or has expired." + case "bad-request": + return "The request was rejected because it was malformed." + case "timeout-or-duplicate": + return "The response parameter has already been validated before." + case "internal-error": + return "An internal error happened while validating the response. The request can be retried." + } + return string(e) +} + +// Error fulfills the error interface +func (e ErrorCode) Error() string { + return e.String() +} diff --git a/modules/util/keypair.go b/modules/util/keypair.go new file mode 100644 index 000000000..5a3ce715a --- /dev/null +++ b/modules/util/keypair.go @@ -0,0 +1,45 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package util + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" +) + +// GenerateKeyPair generates a public and private keypair +func GenerateKeyPair(bits int) (string, string, error) { + priv, _ := rsa.GenerateKey(rand.Reader, bits) + privPem, err := pemBlockForPriv(priv) + if err != nil { + return "", "", err + } + pubPem, err := pemBlockForPub(&priv.PublicKey) + if err != nil { + return "", "", err + } + return privPem, pubPem, nil +} + +func pemBlockForPriv(priv *rsa.PrivateKey) (string, error) { + privBytes := pem.EncodeToMemory(&pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(priv), + }) + return string(privBytes), nil +} + +func pemBlockForPub(pub *rsa.PublicKey) (string, error) { + pubASN1, err := x509.MarshalPKIXPublicKey(pub) + if err != nil { + return "", err + } + pubBytes := pem.EncodeToMemory(&pem.Block{ + Type: "PUBLIC KEY", + Bytes: pubASN1, + }) + return string(pubBytes), nil +} diff --git a/modules/util/keypair_test.go b/modules/util/keypair_test.go new file mode 100644 index 000000000..c6f68c845 --- /dev/null +++ b/modules/util/keypair_test.go @@ -0,0 +1,61 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package util + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/pem" + "regexp" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestKeygen(t *testing.T) { + priv, pub, err := GenerateKeyPair(2048) + assert.NoError(t, err) + + assert.NotEmpty(t, priv) + assert.NotEmpty(t, pub) + + assert.Regexp(t, regexp.MustCompile("^-----BEGIN RSA PRIVATE KEY-----.*"), priv) + assert.Regexp(t, regexp.MustCompile("^-----BEGIN PUBLIC KEY-----.*"), pub) +} + +func TestSignUsingKeys(t *testing.T) { + priv, pub, err := GenerateKeyPair(2048) + assert.NoError(t, err) + + privPem, _ := pem.Decode([]byte(priv)) + if privPem == nil || privPem.Type != "RSA PRIVATE KEY" { + t.Fatal("key is wrong type") + } + + privParsed, err := x509.ParsePKCS1PrivateKey(privPem.Bytes) + assert.NoError(t, err) + + pubPem, _ := pem.Decode([]byte(pub)) + if pubPem == nil || pubPem.Type != "PUBLIC KEY" { + t.Fatal("key failed to decode") + } + + pubParsed, err := x509.ParsePKIXPublicKey(pubPem.Bytes) + assert.NoError(t, err) + + // Sign + msg := "activity pub is great!" + h := sha256.New() + h.Write([]byte(msg)) + d := h.Sum(nil) + sig, err := rsa.SignPKCS1v15(rand.Reader, privParsed, crypto.SHA256, d) + assert.NoError(t, err) + + // Verify + err = rsa.VerifyPKCS1v15(pubParsed.(*rsa.PublicKey), crypto.SHA256, d, sig) + assert.NoError(t, err) +} diff --git a/modules/validation/binding.go b/modules/validation/binding.go index ef0d01e80..1f904979f 100644 --- a/modules/validation/binding.go +++ b/modules/validation/binding.go @@ -8,6 +8,7 @@ import ( "regexp" "strings" + "code.gitea.io/gitea/modules/auth" "code.gitea.io/gitea/modules/git" "gitea.com/go-chi/binding" @@ -17,15 +18,14 @@ import ( const ( // ErrGitRefName is git reference name error ErrGitRefName = "GitRefNameError" - // ErrGlobPattern is returned when glob pattern is invalid ErrGlobPattern = "GlobPattern" - // ErrRegexPattern is returned when a regex pattern is invalid ErrRegexPattern = "RegexPattern" - // ErrUsername is username error ErrUsername = "UsernameError" + // ErrInvalidGroupTeamMap is returned when a group team mapping is invalid + ErrInvalidGroupTeamMap = "InvalidGroupTeamMap" ) // AddBindingRules adds additional binding rules @@ -37,6 +37,7 @@ func AddBindingRules() { addRegexPatternRule() addGlobOrRegexPatternRule() addUsernamePatternRule() + addValidGroupTeamMapRule() } func addGitRefNameBindingRule() { @@ -167,6 +168,23 @@ func addUsernamePatternRule() { }) } +func addValidGroupTeamMapRule() { + binding.AddRule(&binding.Rule{ + IsMatch: func(rule string) bool { + return strings.HasPrefix(rule, "ValidGroupTeamMap") + }, + IsValid: func(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) { + _, err := auth.UnmarshalGroupTeamMapping(fmt.Sprintf("%v", val)) + if err != nil { + errs.Add([]string{name}, ErrInvalidGroupTeamMap, err.Error()) + return false, errs + } + + return true, errs + }, + }) +} + func portOnly(hostport string) string { colon := strings.IndexByte(hostport, ':') if colon == -1 { diff --git a/modules/web/middleware/binding.go b/modules/web/middleware/binding.go index 733f00a1d..8b74a864d 100644 --- a/modules/web/middleware/binding.go +++ b/modules/web/middleware/binding.go @@ -136,6 +136,8 @@ func Validate(errs binding.Errors, data map[string]interface{}, f Form, l transl data["ErrorMsg"] = trName + l.Tr("form.regex_pattern_error", errs[0].Message) case validation.ErrUsername: data["ErrorMsg"] = trName + l.Tr("form.username_error") + case validation.ErrInvalidGroupTeamMap: + data["ErrorMsg"] = trName + l.Tr("form.invalid_group_team_map_error", errs[0].Message) default: msg := errs[0].Classification if msg != "" && errs[0].Message != "" { |