Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions modules/markup/html_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ func TestRender_email(t *testing.T) {

func TestRender_emoji(t *testing.T) {
setting.AppURL = markup.TestAppURL
setting.StaticURLPrefix = markup.TestAppURL
setting.StaticURLPrefix = strings.TrimSuffix(markup.TestAppURL, "/")

test := func(input, expected string) {
expected = strings.ReplaceAll(expected, "&", "&")
Expand Down Expand Up @@ -500,7 +500,7 @@ func Test_ParseClusterFuzz(t *testing.T) {
}

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

test := func(input, expected string) {
Expand Down
71 changes: 1 addition & 70 deletions modules/setting/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
package setting

import (
"encoding/base64"
"net"
"net/url"
"os"
Expand All @@ -13,7 +12,6 @@ import (
"strings"
"time"

"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
)

Expand Down Expand Up @@ -112,72 +110,9 @@ var (
StartupTimeout time.Duration
PerWriteTimeout = 30 * time.Second
PerWritePerKbTimeout = 10 * time.Second
StaticURLPrefix string
AbsoluteAssetURL string

ManifestData string
StaticURLPrefix string // no trailing slash, defaults to AppSubURL, the URL can be relative or absolute
)

// MakeManifestData generates web app manifest JSON
func MakeManifestData(appName, appURL, absoluteAssetURL string) []byte {
type manifestIcon struct {
Src string `json:"src"`
Type string `json:"type"`
Sizes string `json:"sizes"`
}

type manifestJSON struct {
Name string `json:"name"`
ShortName string `json:"short_name"`
StartURL string `json:"start_url"`
Icons []manifestIcon `json:"icons"`
}

bytes, err := json.Marshal(&manifestJSON{
Name: appName,
ShortName: appName,
StartURL: appURL,
Icons: []manifestIcon{
{
Src: absoluteAssetURL + "/assets/img/logo.png",
Type: "image/png",
Sizes: "512x512",
},
{
Src: absoluteAssetURL + "/assets/img/logo.svg",
Type: "image/svg+xml",
Sizes: "512x512",
},
},
})
if err != nil {
log.Error("unable to marshal manifest JSON. Error: %v", err)
return make([]byte, 0)
}

return bytes
}

// MakeAbsoluteAssetURL returns the absolute asset url prefix without a trailing slash
func MakeAbsoluteAssetURL(appURL *url.URL, staticURLPrefix string) string {
parsedPrefix, err := url.Parse(strings.TrimSuffix(staticURLPrefix, "/"))
if err != nil {
log.Fatal("Unable to parse STATIC_URL_PREFIX: %v", err)
}

if err == nil && parsedPrefix.Hostname() == "" {
if staticURLPrefix == "" {
return strings.TrimSuffix(appURL.String(), "/")
}

// StaticURLPrefix is just a path
appHostURL := &url.URL{Scheme: appURL.Scheme, Host: appURL.Host}
return appHostURL.String() + "/" + strings.Trim(staticURLPrefix, "/")
}

return strings.TrimSuffix(staticURLPrefix, "/")
}

func loadServerFrom(rootCfg ConfigProvider) {
sec := rootCfg.Section("server")
AppName = rootCfg.Section("").Key("APP_NAME").MustString("Gitea: Git with a cup of tea")
Expand Down Expand Up @@ -313,10 +248,6 @@ func loadServerFrom(rootCfg ConfigProvider) {
Domain = urlHostname
}

AbsoluteAssetURL = MakeAbsoluteAssetURL(appURL, StaticURLPrefix)
manifestBytes := MakeManifestData(AppName, AppURL, AbsoluteAssetURL)
ManifestData = `application/json;base64,` + base64.StdEncoding.EncodeToString(manifestBytes)

var defaultLocalURL string
switch Protocol {
case HTTPUnix:
Expand Down
41 changes: 0 additions & 41 deletions modules/setting/setting_test.go

This file was deleted.

28 changes: 28 additions & 0 deletions routers/web/misc/misc.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,44 @@ import (
"net/http"
"path"
"strconv"
"strings"

"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/services/context"
)

func SiteManifest(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "application/manifest+json")
// start_url and icon URLs are derived from the request, so a shared cache
// must not reuse the response across hosts; use a private cache instead.
if httpcache.HandleGenericETagPrivateCache(req, w, "", &setting.AppStartTime) {
Comment thread
wxiaoguang marked this conversation as resolved.
Outdated
return
}
if req.Method == http.MethodHead {
return
}

ctx := req.Context()
absoluteAssetURL := strings.TrimSuffix(httplib.MakeAbsoluteURL(ctx, setting.StaticURLPrefix), "/")
manifest := map[string]any{
"name": setting.AppName,
"short_name": setting.AppName,
"start_url": httplib.GuessCurrentAppURL(ctx),
"icons": []map[string]string{
{"src": absoluteAssetURL + "/assets/img/logo.png", "type": "image/png", "sizes": "512x512"},
{"src": absoluteAssetURL + "/assets/img/logo.svg", "type": "image/svg+xml", "sizes": "512x512"},
},
}
_ = json.NewEncoder(w).Encode(manifest)
}

func SSHInfo(rw http.ResponseWriter, req *http.Request) {
if !git.DefaultFeatures().SupportProcReceive {
rw.WriteHeader(http.StatusNotFound)
Expand Down
1 change: 1 addition & 0 deletions routers/web/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ func Routes() *web.Router {
routes.BeforeRouting(chi_middleware.GetHead)

routes.Head("/", misc.DummyOK) // for health check - doesn't need to be passed through gzip handler
routes.Methods("GET, HEAD", "/assets/site-manifest.json", misc.SiteManifest)
routes.Methods("GET, HEAD, OPTIONS", "/assets/*", routing.MarkLogLevelTrace, public.AssetsCors(), public.FileHandlerFunc())
routes.Methods("GET, HEAD", "/avatars/*", avatarStorageHandler(setting.Avatar.Storage, "avatars", storage.Avatars))
routes.Methods("GET, HEAD", "/repo-avatars/*", avatarStorageHandler(setting.RepoAvatar.Storage, "repo-avatars", storage.RepoAvatars))
Expand Down
1 change: 0 additions & 1 deletion services/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,6 @@ func Contexter() func(next http.Handler) http.Handler {
ctx.Data["DisableStars"] = setting.Repository.DisableStars
ctx.Data["EnableActions"] = setting.Actions.Enabled && !unit.TypeActions.UnitGlobalDisabled()

ctx.Data["ManifestData"] = setting.ManifestData
ctx.Data["AllLangs"] = translation.AllLangs()

next.ServeHTTP(ctx.Resp, ctx.Req)
Expand Down
3 changes: 1 addition & 2 deletions services/context/context_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,7 @@ func (c TemplateContext) HeadMetaContentSecurityPolicy() template.HTML {
// * Maybe this approach should be avoided, don't make the config system too complex, just let users use A
return template.HTML(`<meta http-equiv="Content-Security-Policy" content="` +
// allow all by default (the same as old releases with no CSP)
// "data:" is used to load the manifest in head (maybe also need to be refactored in the future)
// maybe some images are also loaded by "data:", need to investigate
// maybe some images or markup (external) renders need "data:", need to investigate
`default-src * data:;` +

// enforce nonce for all scripts, disallow inline scripts
Expand Down
2 changes: 1 addition & 1 deletion templates/base/head.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
{{ctx.HeadMetaContentSecurityPolicy}}
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{if .Title}}{{.Title}} - {{end}}{{.PageTitleCommon}}</title>
{{if .ManifestData}}<link rel="manifest" href="data:{{.ManifestData}}">{{end}}
<link rel="manifest" href="{{AssetUrlPrefix}}/site-manifest.json">
<meta name="author" content="{{if .Repository}}{{.Owner.Name}}{{else}}{{MetaAuthor}}{{end}}">
<meta name="description" content="{{if .Repository}}{{.Repository.Name}}{{if .Repository.Description}} - {{.Repository.Description}}{{end}}{{else}}{{MetaDescription}}{{end}}">
<meta name="keywords" content="{{MetaKeywords}}">
Expand Down
32 changes: 32 additions & 0 deletions tests/integration/view_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@
package integration

import (
"fmt"
"net/http"
"strings"
"testing"

"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/tests"

"github.com/stretchr/testify/assert"
Expand All @@ -17,6 +20,7 @@ func TestView(t *testing.T) {
t.Run("RenderFileSVGIsInImgTag", testRenderFileSVGIsInImgTag)
t.Run("CommitListActions", testCommitListActions)
t.Run("SecurityHeadersDefaults", testSecurityHeadersDefaults)
t.Run("SiteManifest", testSiteManifest)
}

func testRenderFileSVGIsInImgTag(t *testing.T) {
Expand Down Expand Up @@ -81,3 +85,31 @@ func testSecurityHeadersDefaults(t *testing.T) {
assertSecurityHeaders(t, "/api/v1/version")
assertSecurityHeaders(t, "/assets/img/favicon.png")
}

func testSiteManifest(t *testing.T) {
req := NewRequest(t, "GET", "/")
resp := MakeRequest(t, req, http.StatusOK)
assert.Contains(t, resp.Body.String(), `<link rel="manifest" href="/assets/site-manifest.json">`)

req = NewRequest(t, "GET", "/assets/site-manifest.json")
resp = MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "application/manifest+json", resp.Header().Get("Content-Type"))

assetBase := strings.TrimSuffix(setting.AppURL, "/")
expectedJSON := fmt.Sprintf(`{
"name": %q,
"short_name": %q,
"start_url": %q,
"icons": [
{"src": %q, "type": "image/png", "sizes": "512x512"},
{"src": %q, "type": "image/svg+xml", "sizes": "512x512"}
Comment thread
wxiaoguang marked this conversation as resolved.
Outdated
]
}`,
setting.AppName,
setting.AppName,
setting.AppURL,
assetBase+"/assets/img/logo.png",
assetBase+"/assets/img/logo.svg",
)
assert.JSONEq(t, expectedJSON, resp.Body.String())
}
Loading