Skip to content

Commit 696e792

Browse files
committed
Merge remote-tracking branch 'origin/main' into pr-36965-ci
* origin/main: Stabilize e2e logout propagation test (go-gitea#37403) refactor: serve site manifest via `/assets/site-manifest.json` endpoint (go-gitea#37405) feat(security): set X-Content-Type-Options: nosniff by default (go-gitea#37354) # Conflicts: # tests/e2e/events.test.ts
2 parents e483e4c + 0277e3e commit 696e792

10 files changed

Lines changed: 83 additions & 136 deletions

File tree

modules/markup/html_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ func TestRender_email(t *testing.T) {
317317

318318
func TestRender_emoji(t *testing.T) {
319319
setting.AppURL = markup.TestAppURL
320-
setting.StaticURLPrefix = markup.TestAppURL
320+
setting.StaticURLPrefix = strings.TrimSuffix(markup.TestAppURL, "/")
321321

322322
test := func(input, expected string) {
323323
expected = strings.ReplaceAll(expected, "&", "&")
@@ -500,7 +500,7 @@ func Test_ParseClusterFuzz(t *testing.T) {
500500
}
501501

502502
func TestPostProcess(t *testing.T) {
503-
setting.StaticURLPrefix = markup.TestAppURL // can't run standalone
503+
setting.StaticURLPrefix = strings.TrimSuffix(markup.TestAppURL, "/") // can't run standalone
504504
defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
505505

506506
test := func(input, expected string) {

modules/setting/server.go

Lines changed: 1 addition & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
package setting
55

66
import (
7-
"encoding/base64"
87
"net"
98
"net/url"
109
"os"
@@ -13,7 +12,6 @@ import (
1312
"strings"
1413
"time"
1514

16-
"code.gitea.io/gitea/modules/json"
1715
"code.gitea.io/gitea/modules/log"
1816
)
1917

@@ -112,72 +110,9 @@ var (
112110
StartupTimeout time.Duration
113111
PerWriteTimeout = 30 * time.Second
114112
PerWritePerKbTimeout = 10 * time.Second
115-
StaticURLPrefix string
116-
AbsoluteAssetURL string
117-
118-
ManifestData string
113+
StaticURLPrefix string // no trailing slash, defaults to AppSubURL, the URL can be relative or absolute
119114
)
120115

121-
// MakeManifestData generates web app manifest JSON
122-
func MakeManifestData(appName, appURL, absoluteAssetURL string) []byte {
123-
type manifestIcon struct {
124-
Src string `json:"src"`
125-
Type string `json:"type"`
126-
Sizes string `json:"sizes"`
127-
}
128-
129-
type manifestJSON struct {
130-
Name string `json:"name"`
131-
ShortName string `json:"short_name"`
132-
StartURL string `json:"start_url"`
133-
Icons []manifestIcon `json:"icons"`
134-
}
135-
136-
bytes, err := json.Marshal(&manifestJSON{
137-
Name: appName,
138-
ShortName: appName,
139-
StartURL: appURL,
140-
Icons: []manifestIcon{
141-
{
142-
Src: absoluteAssetURL + "/assets/img/logo.png",
143-
Type: "image/png",
144-
Sizes: "512x512",
145-
},
146-
{
147-
Src: absoluteAssetURL + "/assets/img/logo.svg",
148-
Type: "image/svg+xml",
149-
Sizes: "512x512",
150-
},
151-
},
152-
})
153-
if err != nil {
154-
log.Error("unable to marshal manifest JSON. Error: %v", err)
155-
return make([]byte, 0)
156-
}
157-
158-
return bytes
159-
}
160-
161-
// MakeAbsoluteAssetURL returns the absolute asset url prefix without a trailing slash
162-
func MakeAbsoluteAssetURL(appURL *url.URL, staticURLPrefix string) string {
163-
parsedPrefix, err := url.Parse(strings.TrimSuffix(staticURLPrefix, "/"))
164-
if err != nil {
165-
log.Fatal("Unable to parse STATIC_URL_PREFIX: %v", err)
166-
}
167-
168-
if err == nil && parsedPrefix.Hostname() == "" {
169-
if staticURLPrefix == "" {
170-
return strings.TrimSuffix(appURL.String(), "/")
171-
}
172-
173-
// StaticURLPrefix is just a path
174-
appHostURL := &url.URL{Scheme: appURL.Scheme, Host: appURL.Host}
175-
return appHostURL.String() + "/" + strings.Trim(staticURLPrefix, "/")
176-
}
177-
178-
return strings.TrimSuffix(staticURLPrefix, "/")
179-
}
180-
181116
func loadServerFrom(rootCfg ConfigProvider) {
182117
sec := rootCfg.Section("server")
183118
AppName = rootCfg.Section("").Key("APP_NAME").MustString("Gitea: Git with a cup of tea")
@@ -313,10 +248,6 @@ func loadServerFrom(rootCfg ConfigProvider) {
313248
Domain = urlHostname
314249
}
315250

316-
AbsoluteAssetURL = MakeAbsoluteAssetURL(appURL, StaticURLPrefix)
317-
manifestBytes := MakeManifestData(AppName, AppURL, AbsoluteAssetURL)
318-
ManifestData = `application/json;base64,` + base64.StdEncoding.EncodeToString(manifestBytes)
319-
320251
var defaultLocalURL string
321252
switch Protocol {
322253
case HTTPUnix:

modules/setting/setting_test.go

Lines changed: 0 additions & 41 deletions
This file was deleted.

routers/api/v1/api.go

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -865,7 +865,6 @@ func checkDeprecatedAuthMethods(ctx *context.APIContext) {
865865
func Routes() *web.Router {
866866
m := web.NewRouter()
867867

868-
m.BeforeRouting(securityHeaders())
869868
if setting.CORSConfig.Enabled {
870869
m.BeforeRouting(cors.Handler(cors.Options{
871870
AllowedOrigins: setting.CORSConfig.AllowDomain,
@@ -1749,14 +1748,3 @@ func Routes() *web.Router {
17491748

17501749
return m
17511750
}
1752-
1753-
func securityHeaders() func(http.Handler) http.Handler {
1754-
return func(next http.Handler) http.Handler {
1755-
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
1756-
// CORB: https://www.chromium.org/Home/chromium-security/corb-for-developers
1757-
// http://stackoverflow.com/a/3146618/244009
1758-
resp.Header().Set("x-content-type-options", "nosniff")
1759-
next.ServeHTTP(resp, req)
1760-
})
1761-
}
1762-
}

routers/web/misc/misc.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,42 @@ import (
77
"net/http"
88
"path"
99
"strconv"
10+
"strings"
1011

1112
"code.gitea.io/gitea/modules/git"
1213
"code.gitea.io/gitea/modules/httpcache"
14+
"code.gitea.io/gitea/modules/httplib"
15+
"code.gitea.io/gitea/modules/json"
1316
"code.gitea.io/gitea/modules/log"
1417
"code.gitea.io/gitea/modules/setting"
1518
"code.gitea.io/gitea/modules/util"
1619
"code.gitea.io/gitea/modules/web/middleware"
1720
"code.gitea.io/gitea/services/context"
1821
)
1922

23+
func SiteManifest(w http.ResponseWriter, req *http.Request) {
24+
w.Header().Set("Content-Type", "application/manifest+json")
25+
if httpcache.HandleGenericETagPublicCache(req, w, "", &setting.AppStartTime) {
26+
return
27+
}
28+
if req.Method == http.MethodHead {
29+
return
30+
}
31+
32+
ctx := req.Context()
33+
absoluteAssetURL := strings.TrimSuffix(httplib.MakeAbsoluteURL(ctx, setting.StaticURLPrefix), "/")
34+
manifest := map[string]any{
35+
"name": setting.AppName,
36+
"short_name": setting.AppName,
37+
"start_url": httplib.GuessCurrentAppURL(ctx),
38+
"icons": []map[string]string{
39+
{"src": absoluteAssetURL + "/assets/img/logo.png", "type": "image/png", "sizes": "512x512"},
40+
{"src": absoluteAssetURL + "/assets/img/logo.svg", "type": "image/svg+xml", "sizes": "512x512"},
41+
},
42+
}
43+
_ = json.NewEncoder(w).Encode(manifest)
44+
}
45+
2046
func SSHInfo(rw http.ResponseWriter, req *http.Request) {
2147
if !git.DefaultFeatures().SupportProcReceive {
2248
rw.WriteHeader(http.StatusNotFound)

routers/web/web.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ func Routes() *web.Router {
260260
routes.BeforeRouting(chi_middleware.GetHead)
261261

262262
routes.Head("/", misc.DummyOK) // for health check - doesn't need to be passed through gzip handler
263+
routes.Methods("GET, HEAD", "/assets/site-manifest.json", misc.SiteManifest)
263264
routes.Methods("GET, HEAD, OPTIONS", "/assets/*", routing.MarkLogLevelTrace, public.AssetsCors(), public.FileHandlerFunc())
264265
routes.Methods("GET, HEAD", "/avatars/*", avatarStorageHandler(setting.Avatar.Storage, "avatars", storage.Avatars))
265266
routes.Methods("GET, HEAD", "/repo-avatars/*", avatarStorageHandler(setting.RepoAvatar.Storage, "repo-avatars", storage.RepoAvatars))

services/context/context.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -196,10 +196,6 @@ func Contexter() func(next http.Handler) http.Handler {
196196

197197
httpcache.SetCacheControlInHeader(ctx.Resp.Header(), &httpcache.CacheControlOptions{NoTransform: true})
198198

199-
if setting.Security.XFrameOptions != "unset" {
200-
ctx.Resp.Header().Set(`X-Frame-Options`, setting.Security.XFrameOptions)
201-
}
202-
203199
ctx.Data["SystemConfig"] = setting.Config()
204200

205201
ctx.Data["ShowTwoFactorRequiredMessage"] = ctx.DoerNeedTwoFactorAuth()
@@ -209,7 +205,6 @@ func Contexter() func(next http.Handler) http.Handler {
209205
ctx.Data["DisableStars"] = setting.Repository.DisableStars
210206
ctx.Data["EnableActions"] = setting.Actions.Enabled && !unit.TypeActions.UnitGlobalDisabled()
211207

212-
ctx.Data["ManifestData"] = setting.ManifestData
213208
ctx.Data["AllLangs"] = translation.AllLangs()
214209

215210
next.ServeHTTP(ctx.Resp, ctx.Req)

services/context/context_template.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,7 @@ func (c TemplateContext) HeadMetaContentSecurityPolicy() template.HTML {
148148
// * Maybe this approach should be avoided, don't make the config system too complex, just let users use A
149149
return template.HTML(`<meta http-equiv="Content-Security-Policy" content="` +
150150
// allow all by default (the same as old releases with no CSP)
151-
// "data:" is used to load the manifest in head (maybe also need to be refactored in the future)
152-
// maybe some images are also loaded by "data:", need to investigate
151+
// maybe some images or markup (external) renders need "data:", need to investigate
153152
`default-src * data:;` +
154153

155154
// enforce nonce for all scripts, disallow inline scripts

templates/base/head.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
{{ctx.HeadMetaContentSecurityPolicy}}
55
<meta name="viewport" content="width=device-width, initial-scale=1">
66
<title>{{if .Title}}{{.Title}} - {{end}}{{.PageTitleCommon}}</title>
7-
{{if .ManifestData}}<link rel="manifest" href="data:{{.ManifestData}}">{{end}}
7+
<link rel="manifest" href="{{AssetUrlPrefix}}/site-manifest.json">
88
<meta name="author" content="{{if .Repository}}{{.Owner.Name}}{{else}}{{MetaAuthor}}{{end}}">
99
<meta name="description" content="{{if .Repository}}{{.Repository.Name}}{{if .Repository.Description}} - {{.Repository.Description}}{{end}}{{else}}{{MetaDescription}}{{end}}">
1010
<meta name="keywords" content="{{MetaKeywords}}">

tests/integration/view_test.go

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,26 @@
44
package integration
55

66
import (
7+
"fmt"
78
"net/http"
9+
"strings"
810
"testing"
911

12+
"code.gitea.io/gitea/modules/setting"
1013
"code.gitea.io/gitea/tests"
1114

1215
"github.com/stretchr/testify/assert"
1316
)
1417

15-
func TestRenderFileSVGIsInImgTag(t *testing.T) {
18+
func TestView(t *testing.T) {
1619
defer tests.PrepareTestEnv(t)()
20+
t.Run("RenderFileSVGIsInImgTag", testRenderFileSVGIsInImgTag)
21+
t.Run("CommitListActions", testCommitListActions)
22+
t.Run("SecurityHeadersDefaults", testSecurityHeadersDefaults)
23+
t.Run("SiteManifest", testSiteManifest)
24+
}
1725

26+
func testRenderFileSVGIsInImgTag(t *testing.T) {
1827
session := loginUser(t, "user2")
1928

2029
req := NewRequest(t, "GET", "/user2/repo2/src/branch/master/line.svg")
@@ -26,8 +35,7 @@ func TestRenderFileSVGIsInImgTag(t *testing.T) {
2635
assert.Equal(t, "/user2/repo2/raw/branch/master/line.svg", src)
2736
}
2837

29-
func TestCommitListActions(t *testing.T) {
30-
defer tests.PrepareTestEnv(t)()
38+
func testCommitListActions(t *testing.T) {
3139
session := loginUser(t, "user2")
3240

3341
t.Run("WikiRevisionList", func(t *testing.T) {
@@ -65,3 +73,43 @@ func TestCommitListActions(t *testing.T) {
6573
AssertHTMLElement(t, htmlDoc, `.commit-list .view-commit-path`, true)
6674
})
6775
}
76+
77+
func testSecurityHeadersDefaults(t *testing.T) {
78+
assertSecurityHeaders := func(t *testing.T, uri string) {
79+
req := NewRequest(t, "GET", uri)
80+
resp := MakeRequest(t, req, http.StatusOK)
81+
assert.Equal(t, "nosniff", resp.Header().Get("X-Content-Type-Options"))
82+
assert.Equal(t, "SAMEORIGIN", resp.Header().Get("X-Frame-Options"))
83+
}
84+
assertSecurityHeaders(t, "/")
85+
assertSecurityHeaders(t, "/api/v1/version")
86+
assertSecurityHeaders(t, "/assets/img/favicon.png")
87+
}
88+
89+
func testSiteManifest(t *testing.T) {
90+
req := NewRequest(t, "GET", "/")
91+
resp := MakeRequest(t, req, http.StatusOK)
92+
assert.Contains(t, resp.Body.String(), `<link rel="manifest" href="/assets/site-manifest.json">`)
93+
94+
req = NewRequest(t, "GET", "/assets/site-manifest.json")
95+
resp = MakeRequest(t, req, http.StatusOK)
96+
assert.Equal(t, "application/manifest+json", resp.Header().Get("Content-Type"))
97+
98+
assetBase := strings.TrimSuffix(setting.AppURL, "/")
99+
expectedJSON := fmt.Sprintf(`{
100+
"name": %q,
101+
"short_name": %q,
102+
"start_url": %q,
103+
"icons": [
104+
{"src": %q, "type": "image/png", "sizes": "512x512"},
105+
{"src": %q, "type": "image/svg+xml", "sizes": "512x512"}
106+
]
107+
}`,
108+
setting.AppName,
109+
setting.AppName,
110+
setting.AppURL,
111+
assetBase+"/assets/img/logo.png",
112+
assetBase+"/assets/img/logo.svg",
113+
)
114+
assert.JSONEq(t, expectedJSON, resp.Body.String())
115+
}

0 commit comments

Comments
 (0)