krz/skunky-art
Alternative privacy frontend for DeviantArt.
clone: git clone https://gitbay.org/krz/skunky-art.git
1package app
2
3// TODO: implement JSON caching and clean up the code.
4
5import (
6 "crypto/sha1" //nolint:gosec // G505: SHA-1 is a cache-key hash here, not a security primitive
7 "encoding/hex"
8 "io"
9 "os"
10 "strings"
11 "sync"
12 "syscall"
13 "time"
14)
15
16type file struct {
17 Score int
18 Content []byte
19}
20
21var tempFS = make(map[[20]byte]*file)
22var mx = &sync.RWMutex{}
23
24// DownloadAndSendMedia proxies one image from DeviantArt's wixmp CDN to the
25// client, serving it from the on-disk or in-memory cache when enabled. It
26// responds 403 when proxying is turned off for this instance.
27func (s skunkyart) DownloadAndSendMedia(subdomain, path string) {
28 var url strings.Builder
29 url.WriteString("https://images-wixmp-")
30 url.WriteString(subdomain)
31 url.WriteString(".wixmp.com/")
32 url.WriteString(path)
33 if t := s.Args.Get("token"); t != "" {
34 url.WriteString("?token=")
35 url.WriteString(t)
36 }
37
38 var response []byte
39
40 switch {
41 case CFG.Cache.Enabled:
42 fileName := sha1.Sum([]byte(subdomain + path)) //nolint:gosec // G401: cache-key hash, not a security primitive
43 filePath := CFG.Cache.Path + "/" + hex.EncodeToString(fileName[:])
44
45 c := func() {
46 // filePath is built from a SHA-1 of the request, not from user input,
47 // so it cannot escape the cache directory.
48 file, err := os.Open(filePath) //nolint:gosec // G304: path is a hash, not user-controlled
49 if err != nil {
50 dwnld := Download(url.String())
51 if dwnld.Status == 200 && strings.HasPrefix(dwnld.Headers.Get("Content-Type"), "image") {
52 response = dwnld.Body
53 try(os.WriteFile(filePath, response, 0600))
54 } else {
55 s.ReturnHTTPError(dwnld.Status)
56 return
57 }
58 } else {
59 defer func() { try(file.Close()) }()
60 file, e := io.ReadAll(file)
61 try(e)
62 response = file
63 }
64 }
65
66 if CFG.Cache.MemCache {
67 mx.Lock()
68 if tempFS[fileName] == nil {
69 tempFS[fileName] = &file{}
70 }
71 mx.Unlock()
72
73 if tempFS[fileName].Content != nil {
74 response = tempFS[fileName].Content
75 tempFS[fileName].Score += 2
76 break
77 } else {
78 c()
79 go func() {
80 defer restore()
81
82 mx.RLock()
83 tempFS[fileName].Content = response
84 mx.RUnlock()
85
86 for {
87 time.Sleep(1 * time.Minute)
88
89 mx.Lock()
90 if tempFS[fileName].Score <= 0 {
91 delete(tempFS, fileName)
92 mx.Unlock()
93 return
94 }
95 tempFS[fileName].Score--
96 mx.Unlock()
97 }
98 }()
99 }
100 } else {
101 c()
102 }
103 case CFG.Proxy:
104 dwnld := Download(url.String())
105 if dwnld.Status != 200 {
106 s.ReturnHTTPError(dwnld.Status)
107 return
108 }
109 response = dwnld.Body
110 default:
111 s.Writer.WriteHeader(403)
112 response = []byte("Sorry, butt proxy on this instance are disabled.")
113 }
114
115 _, _ = s.Writer.Write(response)
116}
117
118// InitCacheSystem runs the cache rotation loop forever, evicting files past
119// their lifetime and emptying the cache when it outgrows max-size. Run it in its
120// own goroutine.
121func InitCacheSystem() {
122 c := &CFG.Cache
123 for {
124 dir, err := os.ReadDir(c.Path)
125 if err != nil {
126 if os.IsNotExist(err) {
127 try(os.Mkdir(c.Path, 0700))
128 continue
129 }
130 println(err.Error())
131 }
132
133 var total int64
134 for _, file := range dir {
135 fileName := c.Path + "/" + file.Name()
136 fileInfo, err := file.Info()
137 try(err)
138
139 if c.Lifetime != "" {
140 now := time.Now().UnixMilli()
141
142 // Sys() is platform-specific and only documented to be a
143 // *syscall.Stat_t on unix; skip rotation rather than panic
144 // if the filesystem reports something else.
145 if stat, ok := fileInfo.Sys().(*syscall.Stat_t); ok {
146 if statTime(stat)+lifetimeParsed <= now {
147 try(os.RemoveAll(fileName))
148 }
149 }
150 }
151
152 total += fileInfo.Size()
153 // if c.MaxSize != 0 && fileInfo.Size() > c.MaxSize {
154 // try(os.RemoveAll(fileName))
155 // }
156 }
157
158 if c.MaxSize != 0 && total > c.MaxSize {
159 try(os.RemoveAll(c.Path))
160 try(os.Mkdir(c.Path, 0700))
161 }
162
163 time.Sleep(time.Second * time.Duration(c.UpdateInterval))
164 }
165}