Skip to content

Commit 206a7a3

Browse files
committed
add v2 directory
1 parent d60c4b1 commit 206a7a3

53 files changed

Lines changed: 12213 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

v2/auth.go

Lines changed: 421 additions & 0 deletions
Large diffs are not rendered by default.

v2/auth_test.go

Lines changed: 596 additions & 0 deletions
Large diffs are not rendered by default.

v2/avatar/avatar.go

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
// Package avatar implements avatart proxy for oauth and
2+
// defines store interface and implements local (fs), gridfs (mongo) and boltdb stores.
3+
package avatar
4+
5+
import (
6+
"bytes"
7+
"crypto/md5" //nolint gosec
8+
"encoding/hex"
9+
"fmt"
10+
"image"
11+
"image/png"
12+
"io"
13+
"net/http"
14+
"strconv"
15+
"strings"
16+
"time"
17+
18+
"github.com/go-pkgz/rest"
19+
"github.com/rrivera/identicon"
20+
"golang.org/x/image/draw"
21+
22+
"github.com/go-pkgz/auth/logger"
23+
"github.com/go-pkgz/auth/token"
24+
)
25+
26+
// Proxy provides http handler for avatars from avatar.Store
27+
// On user login token will call Put and it will retrieve and save picture locally.
28+
type Proxy struct {
29+
logger.L
30+
Store Store
31+
RoutePath string
32+
URL string
33+
ResizeLimit int
34+
}
35+
36+
// Put stores retrieved avatar to avatar.Store. Gets image from user info. Returns proxied url
37+
func (p *Proxy) Put(u token.User, client *http.Client) (avatarURL string, err error) {
38+
39+
genIdenticon := func(userID string) (avatarURL string, err error) {
40+
b, e := GenerateAvatar(userID)
41+
if e != nil {
42+
return "", fmt.Errorf("no picture for %s: %w", userID, e)
43+
}
44+
// put returns avatar base name, like 123456.image
45+
avatarID, e := p.Store.Put(userID, p.resize(bytes.NewBuffer(b), p.ResizeLimit))
46+
if e != nil {
47+
return "", e
48+
}
49+
50+
p.Logf("[DEBUG] saved identicon avatar to %s, user %q", avatarID, u.Name)
51+
return p.URL + p.RoutePath + "/" + avatarID, nil
52+
}
53+
54+
// no picture for user, try to generate identicon avatar
55+
if u.Picture == "" {
56+
return genIdenticon(u.ID)
57+
}
58+
59+
body, err := p.load(u.Picture, client)
60+
if err != nil {
61+
p.Logf("[DEBUG] failed to fetch avatar from the orig %s, %v", u.Picture, err)
62+
return genIdenticon(u.ID)
63+
}
64+
65+
defer func() {
66+
if e := body.Close(); e != nil {
67+
p.Logf("[WARN] can't close response body, %s", e)
68+
}
69+
}()
70+
71+
avatarID, err := p.Store.Put(u.ID, p.resize(body, p.ResizeLimit)) // put returns avatar base name, like 123456.image
72+
if err != nil {
73+
return "", err
74+
}
75+
76+
p.Logf("[DEBUG] saved avatar from %s to %s, user %q", u.Picture, avatarID, u.Name)
77+
return p.URL + p.RoutePath + "/" + avatarID, nil
78+
}
79+
80+
// load avatar from remote url and return body. Caller has to close the reader
81+
func (p *Proxy) load(url string, client *http.Client) (rc io.ReadCloser, err error) {
82+
// load avatar from remote location
83+
var resp *http.Response
84+
err = retry(5, time.Second, func() error {
85+
var e error
86+
resp, e = client.Get(url)
87+
return e
88+
})
89+
if err != nil {
90+
return nil, fmt.Errorf("failed to fetch avatar from the orig: %w", err)
91+
}
92+
93+
if resp.StatusCode != http.StatusOK {
94+
_ = resp.Body.Close() // caller won't close on error
95+
return nil, fmt.Errorf("failed to get avatar from the orig, status %s", resp.Status)
96+
}
97+
98+
return resp.Body, nil
99+
}
100+
101+
// Handler returns token routes for given provider
102+
func (p *Proxy) Handler(w http.ResponseWriter, r *http.Request) {
103+
104+
if r.Method != "GET" {
105+
w.WriteHeader(http.StatusMethodNotAllowed)
106+
}
107+
elems := strings.Split(r.URL.Path, "/")
108+
avatarID := elems[len(elems)-1]
109+
if !reValidAvatarID.MatchString(avatarID) {
110+
rest.SendErrorJSON(w, r, p.L, http.StatusForbidden, fmt.Errorf("invalid avatar id from %s", r.URL.Path), "can't load avatar")
111+
return
112+
}
113+
114+
// enforce client-side caching
115+
etag := `"` + p.Store.ID(avatarID) + `"`
116+
w.Header().Set("Etag", etag)
117+
w.Header().Set("Cache-Control", "max-age=604800") // 7 days
118+
if match := r.Header.Get("If-None-Match"); match != "" {
119+
etag = strings.TrimPrefix(etag, `"`)
120+
etag = strings.TrimSuffix(etag, `"`)
121+
if match == etag {
122+
w.WriteHeader(http.StatusNotModified)
123+
return
124+
}
125+
}
126+
127+
avReader, size, err := p.Store.Get(avatarID)
128+
if err != nil {
129+
rest.SendErrorJSON(w, r, p.L, http.StatusBadRequest, err, "can't load avatar")
130+
return
131+
}
132+
133+
defer func() {
134+
if e := avReader.Close(); e != nil {
135+
p.Logf("[WARN] can't close avatar reader for %s, %s", avatarID, e)
136+
}
137+
}()
138+
139+
w.Header().Set("Content-Type", "image/*")
140+
w.Header().Set("Content-Length", strconv.Itoa(size))
141+
w.WriteHeader(http.StatusOK)
142+
if _, err = io.Copy(w, avReader); err != nil {
143+
p.Logf("[WARN] can't send response to %s, %s", r.RemoteAddr, err)
144+
}
145+
}
146+
147+
// resize an image of supported format (PNG, JPG, GIF) to the size of "limit" px of the biggest side
148+
// (width or height) preserving aspect ratio.
149+
// Returns original reader if resizing is not needed or failed.
150+
func (p *Proxy) resize(reader io.Reader, limit int) io.Reader {
151+
if reader == nil {
152+
p.Logf("[WARN] avatar resize(): reader is nil")
153+
return nil
154+
}
155+
if limit <= 0 {
156+
p.Logf("[DEBUG] avatar resize(): limit should be greater than 0")
157+
return reader
158+
}
159+
160+
var teeBuf bytes.Buffer
161+
tee := io.TeeReader(reader, &teeBuf)
162+
src, _, err := image.Decode(tee)
163+
if err != nil {
164+
p.Logf("[WARN] avatar resize(): can't decode avatar image, %s", err)
165+
return &teeBuf
166+
}
167+
168+
bounds := src.Bounds()
169+
w, h := bounds.Dx(), bounds.Dy()
170+
if w <= limit && h <= limit || w <= 0 || h <= 0 {
171+
p.Logf("[DEBUG] resizing image is smaller that the limit or has 0 size")
172+
return &teeBuf
173+
}
174+
newW, newH := w*limit/h, limit
175+
if w > h {
176+
newW, newH = limit, h*limit/w
177+
}
178+
m := image.NewRGBA(image.Rect(0, 0, newW, newH))
179+
// Slower than `draw.ApproxBiLinear.Scale()` but better quality.
180+
draw.BiLinear.Scale(m, m.Bounds(), src, src.Bounds(), draw.Src, nil)
181+
182+
var out bytes.Buffer
183+
if err = png.Encode(&out, m); err != nil {
184+
p.Logf("[WARN] avatar resize(): can't encode resized avatar to PNG, %s", err)
185+
return &teeBuf
186+
}
187+
return &out
188+
}
189+
190+
// GenerateAvatar for give user with identicon
191+
func GenerateAvatar(user string) ([]byte, error) {
192+
193+
iconGen, err := identicon.New("pkgz/auth", 5, 5)
194+
if err != nil {
195+
return nil, fmt.Errorf("can't create identicon service: %w", err)
196+
}
197+
198+
ii, err := iconGen.Draw(user) // generate an IdentIcon
199+
if err != nil {
200+
return nil, fmt.Errorf("failed to draw avatar for %s: %w", user, err)
201+
}
202+
203+
buf := &bytes.Buffer{}
204+
err = ii.Png(300, buf)
205+
return buf.Bytes(), err
206+
}
207+
208+
// GetGravatarURL returns url to gravatar picture for given email
209+
func GetGravatarURL(email string) (res string, err error) {
210+
211+
hash := md5.Sum([]byte(strings.ToLower(strings.TrimSpace(email))))
212+
hexHash := hex.EncodeToString(hash[:])
213+
214+
client := http.Client{Timeout: 5 * time.Second}
215+
res = "https://www.gravatar.com/avatar/" + hexHash
216+
resp, err := client.Get(res + "?d=404&s=80")
217+
if err != nil {
218+
return "", err
219+
}
220+
defer resp.Body.Close() //nolint gosec // we don't care about response body
221+
if resp.StatusCode != 200 {
222+
return "", fmt.Errorf("%s", resp.Status)
223+
}
224+
return res, nil
225+
}
226+
227+
func retry(retries int, delay time.Duration, fn func() error) (err error) {
228+
for i := 0; i < retries; i++ {
229+
if err = fn(); err == nil {
230+
return nil
231+
}
232+
time.Sleep(delay)
233+
}
234+
if err != nil {
235+
return fmt.Errorf("retry failed: %w", err)
236+
}
237+
return nil
238+
}

0 commit comments

Comments
 (0)