|
|
|
package assets
|
|
|
|
|
|
|
|
import (
|
|
|
|
"XProxy/next/logger"
|
|
|
|
"bytes"
|
|
|
|
"errors"
|
|
|
|
"github.com/andybalholm/brotli"
|
|
|
|
"github.com/avast/retry-go"
|
|
|
|
"github.com/go-http-utils/headers"
|
|
|
|
"github.com/klauspost/compress/flate"
|
|
|
|
"github.com/klauspost/compress/gzip"
|
|
|
|
"io"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
|
|
|
const DownloadRetry = 3 // max retry times
|
|
|
|
const DownloadTimeout = 480 // max request seconds
|
|
|
|
|
|
|
|
// broltiDecode handles brolti encoding in http responses.
|
|
|
|
func broltiDecode(stream io.Reader) ([]byte, error) {
|
|
|
|
var buffer bytes.Buffer
|
|
|
|
_, err := io.Copy(&buffer, brotli.NewReader(stream))
|
|
|
|
if err != nil {
|
|
|
|
logger.Errorf("Failed to decode http responses with brolti encoding -> %v", err)
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return buffer.Bytes(), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// gzipDecode handles gzip encoding in http responses.
|
|
|
|
func gzipDecode(stream io.Reader) ([]byte, error) {
|
|
|
|
reader, err := gzip.NewReader(stream)
|
|
|
|
if err != nil {
|
|
|
|
logger.Errorf("Failed to decode http responses with gzip encoding -> %v", err)
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var buffer bytes.Buffer
|
|
|
|
_, err = io.Copy(&buffer, reader)
|
|
|
|
if err != nil {
|
|
|
|
logger.Errorf("Failed to handle gzip reader -> %v", err)
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return buffer.Bytes(), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// deflateDecode handles deflate encoding in http responses.
|
|
|
|
func deflateDecode(stream io.Reader) ([]byte, error) {
|
|
|
|
var buffer bytes.Buffer
|
|
|
|
_, err := io.Copy(&buffer, flate.NewReader(stream))
|
|
|
|
if err != nil {
|
|
|
|
logger.Errorf("Failed to decode http responses with deflate encoding -> %v", err)
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return buffer.Bytes(), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// nonDecode handles plain encoding in http responses.
|
|
|
|
func nonDecode(stream io.Reader) ([]byte, error) {
|
|
|
|
var buffer bytes.Buffer
|
|
|
|
_, err := io.Copy(&buffer, stream)
|
|
|
|
if err != nil {
|
|
|
|
logger.Errorf("Failed to read http responses -> %v", err)
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return buffer.Bytes(), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// createClient build http client based on http or socks proxy url.
|
|
|
|
func createClient(remoteUrl string, proxyUrl string) (http.Client, error) {
|
|
|
|
if proxyUrl == "" {
|
|
|
|
logger.Infof("Downloading `%s` without proxy", remoteUrl)
|
|
|
|
return http.Client{
|
|
|
|
Timeout: DownloadTimeout * time.Second,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
logger.Infof("Downloading `%s` via `%s`", remoteUrl, proxyUrl)
|
|
|
|
proxy, err := url.Parse(proxyUrl)
|
|
|
|
if err != nil {
|
|
|
|
logger.Errorf("Invalid proxy url `%s` -> %v", proxyUrl, err)
|
|
|
|
return http.Client{}, err
|
|
|
|
}
|
|
|
|
return http.Client{
|
|
|
|
Transport: &http.Transport{
|
|
|
|
Proxy: http.ProxyURL(proxy),
|
|
|
|
},
|
|
|
|
Timeout: DownloadTimeout * time.Second,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// doRequest initiates http request and allows retries.
|
|
|
|
func doRequest(client http.Client, req *http.Request) (*http.Response, error) {
|
|
|
|
maxRetry := retry.Attempts(DownloadRetry)
|
|
|
|
maxDelay := retry.MaxDelay(DownloadTimeout * time.Second)
|
|
|
|
onRetry := retry.OnRetry(func(n uint, err error) {
|
|
|
|
logger.Errorf("Failed to execute http request -> %v", err)
|
|
|
|
if DownloadRetry != n+1 {
|
|
|
|
logger.Infof("Download retry on the %d times, remain %d times...", n+1, DownloadRetry-n-1)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
delay := retry.Delay(4 * time.Second) // backoff start at 4s
|
|
|
|
|
|
|
|
var resp *http.Response
|
|
|
|
retryFunc := func() error {
|
|
|
|
var err error
|
|
|
|
if resp, err = client.Do(req); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
if err := retry.Do(retryFunc, delay, maxDelay, onRetry, maxRetry); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return resp, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// assetDate attempts to obtain the last modification time of the remote
|
|
|
|
// file and returns nil if it does not exist or is invalid.
|
|
|
|
func assetDate(resp *http.Response) *time.Time {
|
|
|
|
date, err := http.ParseTime(resp.Header.Get(headers.LastModified))
|
|
|
|
if err != nil {
|
|
|
|
logger.Warnf("Unable to get remote data modification time")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
logger.Debugf("Remote data modification time -> `%v`", date)
|
|
|
|
return &date
|
|
|
|
}
|
|
|
|
|
|
|
|
// downloadAsset obtains resource file from the remote server, gets its
|
|
|
|
// modification time, and supports proxy acquisition.
|
|
|
|
func downloadAsset(url string, proxy string) ([]byte, *time.Time, error) {
|
|
|
|
client, err := createClient(url, proxy)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
|
|
if err != nil {
|
|
|
|
logger.Errorf("Failed to create http request -> %v", err)
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
req.Header.Set(headers.AcceptEncoding, "gzip, deflate, br")
|
|
|
|
|
|
|
|
resp, err := doRequest(client, req)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
|
|
logger.Errorf("Download `%s` with http status code -> %d", url, resp.StatusCode)
|
|
|
|
return nil, nil, errors.New("http status code non-2xx")
|
|
|
|
}
|
|
|
|
logger.Debugf("Remote data downloaded successfully")
|
|
|
|
|
|
|
|
var content []byte
|
|
|
|
switch resp.Header.Get(headers.ContentEncoding) {
|
|
|
|
case "br":
|
|
|
|
logger.Debugf("Downloaded content using brolti encoding")
|
|
|
|
content, err = broltiDecode(resp.Body)
|
|
|
|
case "gzip":
|
|
|
|
logger.Debugf("Downloaded content using gzip encoding")
|
|
|
|
content, err = gzipDecode(resp.Body)
|
|
|
|
case "deflate":
|
|
|
|
logger.Debugf("Downloaded content using deflate encoding")
|
|
|
|
content, err = deflateDecode(resp.Body)
|
|
|
|
default:
|
|
|
|
content, err = nonDecode(resp.Body)
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
logger.Debugf("Download `%s` successfully -> %d bytes", url, len(content))
|
|
|
|
return content, assetDate(resp), nil
|
|
|
|
}
|