-
Notifications
You must be signed in to change notification settings - Fork 13
/
Copy pathinstaller.go
327 lines (285 loc) · 7.71 KB
/
installer.go
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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
package web
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"net/http"
"os"
"strings"
"time"
"github.com/ImpactDevelopment/ImpactServer/src/util"
"github.com/google/uuid"
"archive/zip"
"github.com/labstack/echo/v4"
)
var installerVersion string
type InstallerVersion int
const (
JAR InstallerVersion = iota
EXE
)
type Entry struct { // can't use zip.Entry since that seeks within the input and decompresses on the fly (slow)
name string
data []byte
}
var installerEntries []Entry
var exeHeader []byte
var ready = make(chan struct{})
func (version InstallerVersion) getEXT() string {
if version == JAR {
return "jar"
} else {
return "exe"
}
}
func (version InstallerVersion) getURL() string {
return "https://github.com/ImpactDevelopment/Installer/releases/download/" + installerVersion + "/installer-" + installerVersion + "." + version.getEXT()
}
func (version InstallerVersion) fetchFile() ([]byte, error) {
url := version.getURL()
fmt.Println("Downloading", url)
request, err := util.GetRequest(url)
if err != nil {
return nil, err
}
response, err := request.Do()
if err != nil {
return nil, err
}
if !response.Ok() {
return nil, errors.New("Installer download status not OK")
}
data := response.Body
fmt.Println("Finished downloading", url, "length is", len(data))
return data, err
}
func (version InstallerVersion) incrementGithubDownloadCountButDontActuallyUseTheirS3Bandwidth() {
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
resp, err := client.Get(version.getURL())
if err != nil {
fmt.Println(err)
}
if resp.StatusCode != 302 {
fmt.Println("GitHub did not accept the request")
}
}
func init() {
installerVersion = os.Getenv("INSTALLER_VERSION")
if installerVersion == "" {
fmt.Println("WARNING: Installer version not specified, download will not work!")
return
}
// fetch the files on startup, but don't block init on it :brain:
go startup()
}
func startup() {
// Download the two files in parallel
jarCh := make(chan []byte)
exeCh := make(chan []byte)
go func() { jarCh <- downloadUntilSuccess(JAR, 5*time.Minute) }()
go func() { exeCh <- downloadUntilSuccess(EXE, 5*time.Minute) }()
var installerJar = <-jarCh
var installerExe = <-exeCh
zipReader, err := zip.NewReader(bytes.NewReader(installerJar), int64(len(installerJar)))
if err != nil {
panic(err)
}
installerEntries = make([]Entry, 0)
for _, file := range zipReader.File {
entryReader, err := file.Open()
if err != nil {
panic(err)
}
defer entryReader.Close()
data, err := ioutil.ReadAll(entryReader)
if err != nil {
panic(err)
}
installerEntries = append(installerEntries, Entry{
name: file.Name,
data: data,
})
}
exeHeaderLen := len(installerExe) - len(installerJar)
for i := 0; i < len(installerJar); i++ {
if installerJar[i] != installerExe[exeHeaderLen+i] {
panic("invalid installer files")
}
}
exeHeader = installerExe[:exeHeaderLen]
fmt.Println("Initialized")
go func() {
for {
ready <- struct{}{} // we are ready from now on
}
}()
}
// download the installer, if an error occurs keep trying again after duration (blocking)
func downloadUntilSuccess(version InstallerVersion, duration time.Duration) []byte {
var attempts int
exe, err := version.fetchFile()
for err != nil {
attempts++
fmt.Fprintf(os.Stderr, "Error downloading %s Installer after %d attempts: %s\n", version.getEXT(), attempts, err.Error())
time.Sleep(duration)
exe, err = version.fetchFile()
}
return exe
}
// blocks and only returns once startup is done or timeout is complete
// true if startup is done
func awaitStartup(timeout time.Duration) bool {
ticker := time.NewTicker(timeout)
select {
case <-ready:
return true
case <-ticker.C:
return false
}
}
func extractOrGenerateCID(c echo.Context) string {
cid := extractTrackyTracky(c)
if cid != "" {
return cid
}
uuid, err := uuid.NewUUID()
if err != nil {
panic(err) // happens when system clock is not set or something dummy like that
}
return uuid.String()
}
func extractTrackyTracky(c echo.Context) string {
cookie, err := c.Cookie("_ga")
if err != nil {
return ""
}
parts := strings.Split(cookie.Value, ".")
if len(parts) != 4 {
return ""
}
return parts[2] + "." + parts[3]
}
func installerForJar(c echo.Context) error {
return installer(c, JAR)
}
func installerForExe(c echo.Context) error {
return installer(c, EXE)
}
func analytics(cid string, version InstallerVersion, c echo.Context) {
form := map[string]string{
"v": "1",
"t": "event",
"tid": "UA-143397381-1",
"cid": cid,
"ds": "backend",
"ec": "installer",
"ea": "download",
"el": version.getEXT(),
"ua": c.Request().UserAgent(),
}
forward := util.RealIPIfUnambiguous(c)
if forward != "" {
form["uip"] = forward
}
req, err := util.FormRequest("https://www.google-analytics.com/collect", form)
if err != nil {
fmt.Println("Analytics failed to build request", err)
return
}
req.SetHeader("User-Agent", c.Request().UserAgent())
resp, err := req.Do()
if err != nil {
fmt.Println("Analytics error", err)
return
}
if !resp.Ok() {
fmt.Println("Analytics bad status code", resp.Status())
data := resp.String()
fmt.Println(err)
fmt.Println(data)
}
}
func makeEntry(zipWriter *zip.Writer, entryName string, entry []byte, version InstallerVersion) error {
// make an entry with a valid last modified time so as to not crash java 12 reeee
header := &zip.FileHeader{
Name: entryName,
Method: zip.Deflate,
}
switch version {
case EXE: // Don't set modified time for EXE versions
default:
header.Modified = time.Now()
}
// make the entry
writer, err := zipWriter.CreateHeader(header)
if err != nil {
return err
}
_, err = writer.Write([]byte(entry))
if err != nil {
return err
}
return nil
}
func installer(c echo.Context, version InstallerVersion) error {
if installerVersion == "" {
return echo.NewHTTPError(http.StatusInternalServerError, "Installer version not specified")
}
referer := c.Request().Referer()
if referer != "" && !strings.HasPrefix(referer, util.GetServerURL().String()) && !strings.Contains(referer, "brady-money-grubbing-completed") {
fmt.Println("BLOCKING referer", referer)
return echo.NewHTTPError(http.StatusUnauthorized, "no hotlinking >:(")
}
if !awaitStartup(5 * time.Second) {
c.Response().Header().Set("Retry-After", "120")
return echo.NewHTTPError(http.StatusServiceUnavailable, "Installer download not ready yet, please try again later")
}
nightlies := c.QueryParam("nightlies") == "1" || c.QueryParam("nightlies") == "true"
filename := "Impact"
if nightlies {
filename += "Nightly"
}
filename += "Installer-" + installerVersion + "." + version.getEXT()
res := c.Response()
header := res.Header()
header.Set(echo.HeaderContentType, echo.MIMEOctetStream)
header.Set(echo.HeaderContentDisposition, "attachment; filename="+filename)
header.Set("Content-Transfer-Encoding", "binary")
res.WriteHeader(http.StatusOK)
if version == EXE {
_, err := res.Write(exeHeader)
if err != nil {
return err
}
}
zipWriter := zip.NewWriter(res)
defer zipWriter.Close()
for _, entry := range installerEntries {
err := makeEntry(zipWriter, entry.name, entry.data, version)
if err != nil {
return err
}
}
if nightlies {
const properties = "# Enable nightly builds\n" +
"noGPG = true\n" +
"prereleases = true\n"
err := makeEntry(zipWriter, "default_args.properties", []byte(properties), version)
if err != nil {
return err
}
}
cid := extractOrGenerateCID(c)
err := makeEntry(zipWriter, "impact_cid.txt", []byte(cid), version)
if err != nil {
return err
}
go analytics(cid, version, c)
go version.incrementGithubDownloadCountButDontActuallyUseTheirS3Bandwidth()
return nil
}