Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

make avatar lookup occur at image request #10540

Merged
merged 17 commits into from
Mar 27, 2020

Conversation

zeripath
Copy link
Contributor

Speed up page generation by making avatar lookup occur at the browser
not at page generation.

speed up page generation by making avatar lookup occur at the browser
not at page generation
@zeripath zeripath added the topic/ui Change the appearance of the Gitea UI label Feb 28, 2020
@zeripath zeripath added this to the 1.12.0 milestone Feb 28, 2020
@zeripath
Copy link
Contributor Author

This creates a new endpoint /avatar/:email which can be used to obtain the avatar for an email address. I'm not sure if this should have a CSRF control or not. I will have a think.

@GiteaBot GiteaBot added the lgtm/need 2 This PR needs two approvals by maintainers to be considered for merging. label Feb 28, 2020
@lafriks
Copy link
Member

lafriks commented Feb 28, 2020

This will expose email address even if it is set to be private

@zeripath
Copy link
Contributor Author

zeripath commented Feb 29, 2020

@lafriks that's not true.

base.AvatarLink is only used if the email does not match a User. Please see:

// AvatarLink tries to match user in database with e-mail
// in order to show custom avatar, and falls back to general avatar link.
func (pc *PushCommits) AvatarLink(email string) string {
if pc.avatars == nil {
pc.avatars = make(map[string]string)
}
avatar, ok := pc.avatars[email]
if ok {
return avatar
}
u, ok := pc.emailUsers[email]
if !ok {
var err error
u, err = models.GetUserByEmail(email)
if err != nil {
pc.avatars[email] = base.AvatarLink(email)
if !models.IsErrUserNotExist(err) {
log.Error("GetUserByEmail: %v", err)
return ""
}
} else {
pc.emailUsers[email] = u
}
}
if u != nil {
pc.avatars[email] = u.RelAvatarLink()
}
return pc.avatars[email]
}

gitea/routers/repo/blame.go

Lines 223 to 235 in 82be59e

// User avatar image
avatar := ""
commitSince := timeutil.TimeSinceUnix(timeutil.TimeStamp(commit.Author.When.Unix()), ctx.Data["Lang"].(string))
if commit.User != nil {
authorName := commit.Author.Name
if len(commit.User.FullName) > 0 {
authorName = commit.User.FullName
}
avatar = fmt.Sprintf(`<a href="%s/%s"><img class="ui avatar image" src="%s" title="%s" alt=""/></a>`, setting.AppSubURL, url.PathEscape(commit.User.Name), commit.User.RelAvatarLink(), html.EscapeString(authorName))
} else {
avatar = fmt.Sprintf(`<img class="ui avatar image" src="%s" title="%s"/>`, html.EscapeString(base.AvatarLink(commit.Author.Email)), html.EscapeString(commit.Author.Name))
}
commitInfo.WriteString(fmt.Sprintf(`<div class="blame-info%s"><div class="blame-data"><div class="blame-avatar">%s</div><div class="blame-message"><a href="%s/commit/%s" title="%[5]s">%[5]s</a></div><div class="blame-time">%s</div></div></div>`, attr, avatar, repoLink, part.Sha, html.EscapeString(commit.CommitMessage), commitSince))

{{if .Author}}
<img class="ui avatar image" src="{{.Author.RelAvatarLink}}" />
{{if .Author.FullName}}
<a href="{{.Author.HomeLink}}"><strong>{{.Author.FullName}}</strong> {{if .IsSigned}}&lt;{{.Commit.Author.Email}}&gt;{{end}}</a>
{{else}}
<a href="{{.Author.HomeLink}}"><strong>{{.Commit.Author.Name}}</strong> {{if .IsSigned}}&lt;{{.Commit.Author.Email}}&gt;{{end}}</a>
{{end}}
{{else}}
<img class="ui avatar image" src="{{AvatarLink .Commit.Author.Email}}" />
<strong>{{.Commit.Author.Name}}</strong>
{{end}}
{{if or (ne .Commit.Committer.Name .Commit.Author.Name) (ne .Commit.Committer.Email .Commit.Author.Email)}}
<span> </span>
{{if ne .Verification.CommittingUser.ID 0}}
<img class="ui avatar image" src="{{.Verification.CommittingUser.RelAvatarLink}}" />
<a href="{{.Verification.CommittingUser.HomeLink}}"><strong>{{.Commit.Committer.Name}}</strong> &lt;{{.Commit.Committer.Email}}&gt;</a>
{{else}}
<img class="ui avatar image" src="{{AvatarLink .Commit.Committer.Email}}" />
<strong>{{.Commit.Committer.Name}}</strong>
{{end}}
{{end}}

{{if ne .Verification.SigningUser.ID 0}}
<i class="lock icon"></i>
{{if eq .Verification.TrustStatus "trusted"}}
<span class="ui text">{{.i18n.Tr "repo.commits.signed_by"}}:</span>
{{else if eq .Verification.TrustStatus "untrusted"}}
<span class="ui text">{{.i18n.Tr "repo.commits.signed_by_untrusted_user"}}:</span>
{{else}}
<span class="ui text">{{.i18n.Tr "repo.commits.signed_by_untrusted_user_unmatched"}}:</span>
{{end}}
<img class="ui avatar image" src="{{.Verification.SigningUser.RelAvatarLink}}" />
<a href="{{.Verification.SigningUser.HomeLink}}"><strong>{{.Verification.SigningUser.Name}}</strong> <{{.Verification.SigningEmail}}></a>
<span class="pull-right"><span class="ui text">{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span> {{.Verification.SigningKey.KeyID}}</span>
{{else}}
<i class="icons" title="{{.i18n.Tr "gpg.default_key"}}">
<i class="lock icon"></i>
<i class="tiny inverted cog icon centerlock"></i>
</i>
<span class="ui text">{{.i18n.Tr "repo.commits.signed_by"}}:</span>
<img class="ui avatar image" src="{{AvatarLink .Verification.SigningEmail}}" />
<strong>{{.Verification.SigningUser.Name}}</strong> <{{.Verification.SigningEmail}}>
<span class="pull-right"><span class="ui text">{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span> <i class="cogs icon" title="{{.i18n.Tr "gpg.default_key"}}"></i>{{.Verification.SigningKey.KeyID}}</span>
{{end}}

{{if .User}}
{{if .User.FullName}}
{{$userName = .User.FullName}}
{{end}}
<img class="ui avatar image" src="{{.User.RelAvatarLink}}" alt=""/>&nbsp;&nbsp;<a href="{{AppSubUrl}}/{{.User.Name}}">{{$userName}}</a>
{{else}}
<img class="ui avatar image" src="{{AvatarLink .Author.Email}}" alt=""/>&nbsp;&nbsp;{{$userName}}
{{end}}

{{if ne .Verification.SigningUser.ID 0}}
<i class="lock icon"></i>
<img class="ui signature avatar image" src="{{.Verification.SigningUser.RelAvatarLink}}" />
{{else}}
<i title="{{.Verification.Reason}}" class="icons">
<i class="lock icon"></i>
<i class="tiny inverted cog icon centerlock"></i>
</i>
<img class="ui signature avatar image" src="{{AvatarLink .Verification.SigningEmail}}" />
{{end}}

{{if .LatestCommitUser}}
<img class="ui avatar image img-12" src="{{.LatestCommitUser.RelAvatarLink}}" />
{{if .LatestCommitUser.FullName}}
<a href="{{AppSubUrl}}/{{.LatestCommitUser.Name}}"><strong>{{.LatestCommitUser.FullName}}</strong></a>
{{else}}
<a href="{{AppSubUrl}}/{{.LatestCommitUser.Name}}"><strong>{{if .LatestCommit.Author}}{{.LatestCommit.Author.Name}}{{else}}{{.LatestCommitUser.Name}}{{end}}</strong></a>
{{end}}
{{else}}
{{if .LatestCommit.Author}}
<img class="ui avatar image img-12" src="{{AvatarLink .LatestCommit.Author.Email}}" />
<strong>{{.LatestCommit.Author.Name}}</strong>
{{end}}
{{end}}

{{if ne .LatestCommitVerification.SigningUser.ID 0}}
<i class="lock icon"></i>
<img class="ui signature avatar image" src="{{.LatestCommitVerification.SigningUser.RelAvatarLink}}" />
{{else}}
<i title="{{.LatestCommitVerification.Reason}}" class="icons">
<i class="lock icon"></i>
<i class="tiny inverted cog icon centerlock"></i>
</i>
<img class="ui signature avatar image" src="{{AvatarLink .LatestCommitVerification.SigningEmail}}" />
{{end}}

@lunny
Copy link
Member

lunny commented Feb 29, 2020

We should cache the images' thumbnail on backend.

@zeripath
Copy link
Contributor Author

zeripath commented Feb 29, 2020

We already do some caching in libravatar but it's so slow that initial page lookup for any new avatar delays rendering until it is done.

@zeripath
Copy link
Contributor Author

And in any case, browsers are far better at caching than we will ever be.

@zeripath
Copy link
Contributor Author

To be clearer it's not that the users thumbnail takes the time it's this code:

// Finds or defaults a URL for Federation (for openid to be used, email has to be nil)
func (v *Libravatar) baseURL(email *mail.Address, openid *url.URL) (string, error) {
	var service, protocol, domain string

	if v.useHTTPS {
		protocol = "https://"
		service = v.secureServiceBase
		domain = v.secureFallbackHost

	} else {
		protocol = "http:https://"
		service = v.serviceBase
		domain = v.fallbackHost
	}

	host := v.getDomain(email, openid)
	key := cacheKey{service, host}
	now := time.Now()
	v.nameCacheMutex.Lock()
	val, found := v.nameCache[key]
	v.nameCacheMutex.Unlock()
	if found && now.Sub(val.checkedAt) <= v.nameCacheDuration {
		return protocol + val.target, nil
	}

	_, addrs, err := net.LookupSRV(service, "tcp", host)
	if err != nil && err.(*net.DNSError).IsTimeout {
		return "", err
	}

	if len(addrs) == 1 {
		// select only record, if only one is available
		domain = strings.TrimSuffix(addrs[0].Target, ".")
	} else if len(addrs) > 1 {
		// Select first record according to RFC2782 weight
		// ordering algorithm (page 3)

		type record struct {
			srv    *net.SRV
			weight uint16
		}

		var (
			totalWeight uint16
			records     []record
			topPriority = addrs[0].Priority
			topRecord   *net.SRV
		)

		for _, rr := range addrs {
			if rr.Priority > topPriority {
				continue
			} else if rr.Priority < topPriority {
				// won't happen, because net sorts
				// by priority, but just in case
				totalWeight = 0
				records = nil
				topPriority = rr.Priority
			}

			totalWeight += rr.Weight

			if rr.Weight > 0 {
				records = append(records, record{rr, totalWeight})
			} else if rr.Weight == 0 {
				records = append([]record{record{srv: rr, weight: totalWeight}}, records...)
			}
		}

		if len(records) == 1 {
			topRecord = records[0].srv
		} else {
			randnum := uint16(rand.Intn(int(totalWeight)))

			for _, rr := range records {
				if rr.weight >= randnum {
					topRecord = rr.srv
					break
				}
			}
		}

		domain = fmt.Sprintf("%s:%d", topRecord.Target, topRecord.Port)
	}

	v.nameCacheMutex.Lock()
	v.nameCache[key] = cacheValue{checkedAt: now, target: domain}
	v.nameCacheMutex.Unlock()
	return protocol + domain, nil
}

This has to happen for each new domain in an email address.

Without this PR this delay occurs at the time of rendering the page needlessly delaying the appearance of the page by literally seconds.

@codecov-io
Copy link

codecov-io commented Feb 29, 2020

Codecov Report

Merging #10540 into master will increase coverage by 0.03%.
The diff coverage is 29.03%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master   #10540      +/-   ##
==========================================
+ Coverage   43.53%   43.56%   +0.03%     
==========================================
  Files         589      590       +1     
  Lines       82683    82772      +89     
==========================================
+ Hits        35993    36058      +65     
- Misses      42221    42238      +17     
- Partials     4469     4476       +7     
Impacted Files Coverage Δ
modules/base/tool.go 59.09% <0.00%> (-5.93%) ⬇️
modules/templates/helper.go 42.19% <ø> (ø)
routers/repo/blame.go 0.00% <0.00%> (ø)
routers/user/avatar.go 23.25% <0.00%> (-20.23%) ⬇️
modules/cache/cache.go 35.05% <45.83%> (+3.54%) ⬆️
models/avatar.go 57.14% <57.14%> (ø)
models/models.go 59.77% <100.00%> (+0.22%) ⬆️
modules/repository/commits.go 82.85% <100.00%> (ø)
routers/routes/routes.go 86.37% <100.00%> (+0.03%) ⬆️
modules/queue/unique_queue_disk_channel.go 53.84% <0.00%> (-1.93%) ⬇️
... and 12 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update c61b902...bb85931. Read the comment docs.

@silverwind
Copy link
Member

Wouldn't something like AES be more suitable than base64, given that the client does not need to know the email? Or maybe some other lightweight cipher which is not totally broken.

@zeripath
Copy link
Contributor Author

The client will know the email address anyway. The obfuscation is to reduce the chance of the email address being indexed as is.

@zeripath
Copy link
Contributor Author

I would have used the md5sum but libravatar requires the whole unhashed email address - although it could actually get away with the domain and the md5sum.

@silverwind
Copy link
Member

silverwind commented Mar 1, 2020

Why not encrypt/decrypt it using security.SECRET_KEY? Obfuscation is a bad practice and I could see sufficiently advanced spam bots being able to extract a base64-encoded email from HTML, for example on a public gitea instance.

There are existing aesEncrypt and aesDecrypt in models/twofactor.go, I guess those could be extracted to a new file as general AesEncrypt/AesDecrypt. Short strings like email addresses should encrypt/decrypt reasonably fast for avatar usage.

Copy link
Member

@guillep2k guillep2k left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @zeripath, this doesn't expose anything more than is already exposed.

@GiteaBot GiteaBot added lgtm/need 1 This PR needs approval from one additional maintainer to be merged. and removed lgtm/need 2 This PR needs two approvals by maintainers to be considered for merging. labels Mar 1, 2020
@lafriks
Copy link
Member

lafriks commented Mar 1, 2020

I don't think email address is exposed anywhere except when cloning repo and checking commits

@techknowlogick
Copy link
Member

techknowlogick commented Mar 2, 2020

I would have used the md5sum but libravatar requires the whole unhashed email address - although it could actually get away with the domain and the md5sum.

Libravatar uses hash in URL: https://wiki.libravatar.org/api/ although as I am reading this you could mean libravatar library uses the full email in the backend to lookup via DNS the endpoint to use.

Either way I believe @silverwind's suggestion may be best see @guillep2k comment belowe

@guillep2k
Copy link
Member

Either way I believe @silverwind's suggestion may be best

I wouldn't use security.SECRET_KEY "as is", because then a malicious agent could derive the key by knowing the input data (the email address) by pushing some code. A solution to that could be adding some salt to the input value, like 34539493848739845:[email protected]; but for that to work the salt should be different each time and nothing could be cached.

@techknowlogick
Copy link
Member

@guillep2k doh 🤷‍♂, you're right.

@guillep2k
Copy link
Member

I wouldn't use security.SECRET_KEY "as is".

Maybe a new encryption key can be generated each time the instance starts? Caches would be invalidated when the instance restarts, but that wouldn't be so bad.

@silverwind
Copy link
Member

Maybe a new encryption key can be generated each time the instance starts

A runtime-only secret sounds like a good solution here. It doesn't even need to be strongly secure as performance is probably more imporant, but a bit more security than base64 would be nice.

models/avatar.go Outdated Show resolved Hide resolved
routers/user/avatar.go Outdated Show resolved Hide resolved
Signed-off-by: Andrew Thornton <[email protected]>
@guillep2k
Copy link
Member

But of course some unit test will fail....

Signed-off-by: Andrew Thornton <[email protected]>
models/avatar.go Outdated Show resolved Hide resolved
@GiteaBot GiteaBot added lgtm/done This PR has enough approvals to get merged. There are no important open reservations anymore. and removed lgtm/need 1 This PR needs approval from one additional maintainer to be merged. labels Mar 25, 2020
models/avatar.go Outdated Show resolved Hide resolved
models/avatar.go Outdated Show resolved Hide resolved
Copy link
Member

@lafriks lafriks left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good but needs migration

@zeripath
Copy link
Contributor Author

Now it might be worth considering making the user's avatars go through the same hash url system. Then avatar images in emails would work.

@zeripath
Copy link
Contributor Author

Migration to simply create the empty table done.

@lafriks lafriks merged commit e6baa65 into go-gitea:master Mar 27, 2020
@zeripath zeripath deleted the make-avatar-lookup-image branch March 27, 2020 16:39
@go-gitea go-gitea locked and limited conversation to collaborators Nov 24, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
lgtm/done This PR has enough approvals to get merged. There are no important open reservations anymore. topic/ui Change the appearance of the Gitea UI
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

9 participants