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