Skip to content

Commit d26311b

Browse files
committed
Cloudflare and fallback
1 parent 5bb53df commit d26311b

1 file changed

Lines changed: 77 additions & 50 deletions

File tree

‎update-forms-ver.go‎

Lines changed: 77 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,16 @@ import (
44
"archive/zip"
55
"bytes"
66
"crypto/sha1"
7+
"crypto/tls"
78
"encoding/json"
8-
"errors"
99
"fmt"
10-
"html"
1110
"io"
1211
"log"
1312
"net/http"
1413
"os"
15-
"regexp"
1614
"strings"
1715
"time"
16+
"unicode"
1817
)
1918

2019
// KeepAliveToken represents a unique token per calendar month.
@@ -25,6 +24,8 @@ type KeepAliveToken struct{}
2524

2625
func (g KeepAliveToken) MarshalJSON() ([]byte, error) { return json.Marshal(g.String()) }
2726

27+
func (g *KeepAliveToken) UnmarshalJSON([]byte) error { return nil }
28+
2829
func (KeepAliveToken) String() string {
2930
return fmt.Sprintf("%x", sha1.Sum([]byte{byte(time.Now().Month())}))
3031
}
@@ -43,35 +44,83 @@ func (f FormsInfo) String() string {
4344
return fmt.Sprintf("version: '%s', url: '%s'", f.Version, f.ArchiveURL)
4445
}
4546

46-
const FormsInfoURL = "https://www.winlink.org/content/how_manually_update_standard_templates"
4747
const DirectFormsZipURL = "https://downloads.winlink.org/User%20Programs/Standard_Forms.zip"
48+
const MirrorFormsAPIPath = "https://api.getpat.io/v1/forms/standard-templates/"
4849
const PatFormsAPIPath = "https://forms-api.islandmagic.co/v1/forms/standard-templates/"
4950

50-
var client = &http.Client{Timeout: 30 * time.Second}
51+
const browserUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36"
52+
53+
var (
54+
// Cloudflare challenges non-browser HTTP/2 requests to downloads.winlink.org.
55+
formsTransport = http.DefaultTransport.(*http.Transport).Clone()
56+
formsClient = &http.Client{
57+
Timeout: 30 * time.Second,
58+
Transport: formsTransport,
59+
}
60+
)
61+
62+
func init() {
63+
formsTransport.ForceAttemptHTTP2 = false
64+
formsTransport.TLSNextProto = make(map[string]func(string, *tls.Conn) http.RoundTripper)
65+
formsTransport.TLSClientConfig = &tls.Config{NextProtos: []string{"http/1.1"}, MinVersion: tls.VersionTLS12}
66+
}
5167

5268
func main() {
53-
url := DirectFormsZipURL
54-
latest, err := downloadZipURL(url)
69+
latest, err := downloadZipURL(DirectFormsZipURL)
5570
if err != nil {
56-
log.Printf("direct download failed (%v); falling back to HTML scrape", err)
57-
url, err = getLatestFormsUrl()
71+
log.Printf("direct download failed (%v); falling back to api.getpat.io mirror", err)
72+
mirrorURL, err := getMirrorArchiveURL()
5873
if err != nil {
59-
log.Fatalf("could not get latest forms info: %v", err)
74+
log.Fatalf("could not get latest forms info from mirror: %v", err)
6075
}
61-
log.Printf("Found URL %s", url)
62-
latest, err = downloadZipURL(url)
76+
log.Printf("Found mirror URL %s", mirrorURL)
77+
latest, err = downloadZipURL(mirrorURL)
6378
if err != nil {
64-
log.Fatalf("could not download archive url: %v", err)
79+
log.Fatalf("could not download mirror archive url: %v", err)
6580
}
81+
log.Printf("Downloaded from mirror URL %s", mirrorURL)
6682
} else {
67-
log.Printf("Downloaded from direct URL %s", url)
83+
log.Printf("Downloaded from direct URL %s", DirectFormsZipURL)
6884
}
6985
log.Printf("Found version %s", latest.Version)
7086
_ = json.NewEncoder(os.Stdout).Encode(latest)
7187
}
7288

73-
func downloadZipURL(url string) (*FormsInfo, error) {
74-
resp, err := client.Get(url)
89+
func getMirrorArchiveURL() (string, error) {
90+
req, err := http.NewRequest(http.MethodGet, MirrorFormsAPIPath+"latest", nil)
91+
if err != nil {
92+
return "", err
93+
}
94+
req.Header.Set("User-Agent", browserUserAgent)
95+
96+
resp, err := formsClient.Do(req)
97+
if err != nil {
98+
return "", err
99+
}
100+
defer resp.Body.Close()
101+
102+
if resp.StatusCode != http.StatusOK {
103+
return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode)
104+
}
105+
106+
var info FormsInfo
107+
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
108+
return "", fmt.Errorf("decode mirror latest: %w", err)
109+
}
110+
if info.ArchiveURL == "" {
111+
return "", fmt.Errorf("mirror latest response missing archive_url")
112+
}
113+
return info.ArchiveURL, nil
114+
}
115+
116+
func downloadZipURL(zipURL string) (*FormsInfo, error) {
117+
req, err := http.NewRequest(http.MethodGet, zipURL, nil)
118+
if err != nil {
119+
return nil, err
120+
}
121+
req.Header.Set("User-Agent", browserUserAgent)
122+
123+
resp, err := formsClient.Do(req)
75124
if err != nil {
76125
return nil, err
77126
}
@@ -97,7 +146,7 @@ func downloadZipURL(url string) (*FormsInfo, error) {
97146
_, err = io.Copy(out, bytes.NewReader(b))
98147
return &FormsInfo{
99148
Version: version,
100-
ArchiveURL: fmt.Sprintf("%s%s", PatFormsAPIPath, filename),
149+
ArchiveURL: PatFormsAPIPath + filename,
101150
Generated: time.Now(),
102151
}, err
103152
}
@@ -123,7 +172,8 @@ func readAndCheckZip(rc io.ReadCloser) ([]byte, string, error) {
123172
if err != nil {
124173
return nil, "", err
125174
}
126-
version = strings.TrimSpace(string(ver))
175+
// strings.TrimSpace is not sufficient. Version 1.1.6.0 was released as `1.1.6\t.0`
176+
version = stripSpaces(string(ver))
127177
}
128178

129179
// Otherwise, read the file to check for errors but discard the contents
@@ -135,6 +185,15 @@ func readAndCheckZip(rc io.ReadCloser) ([]byte, string, error) {
135185
return body, version, nil
136186
}
137187

188+
func stripSpaces(str string) string {
189+
return strings.Map(func(r rune) rune {
190+
if unicode.IsSpace(r) {
191+
return -1
192+
}
193+
return r
194+
}, str)
195+
}
196+
138197
func readZipFile(zf *zip.File) error {
139198
f, err := zf.Open()
140199
if err != nil {
@@ -153,35 +212,3 @@ func readZipFileContents(zf *zip.File) ([]byte, error) {
153212
defer f.Close()
154213
return io.ReadAll(f)
155214
}
156-
157-
func getLatestFormsUrl() (string, error) {
158-
req, err := http.NewRequest("GET", FormsInfoURL, nil)
159-
if err != nil {
160-
return "", err
161-
}
162-
req.Header.Set("User-Agent", "pat-forms-scraper")
163-
req.Header.Set("Cache-Control", "no-cache")
164-
resp, err := client.Do(req)
165-
if err != nil {
166-
return "", err
167-
}
168-
defer resp.Body.Close()
169-
170-
if resp.StatusCode != http.StatusOK {
171-
return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode)
172-
}
173-
174-
bodyBytes, err := io.ReadAll(resp.Body)
175-
if err != nil {
176-
return "", fmt.Errorf("can't read winlink forms version page: %w", err)
177-
}
178-
bodyString := string(bodyBytes)
179-
180-
// Scrape for the version and download link
181-
hrefRe := regexp.MustCompile(`<a href="(https://.+)">\s*Standard_Forms - Latest Version\s*</a>`)
182-
hrefMatches := hrefRe.FindStringSubmatch(bodyString)
183-
if len(hrefMatches) < 2 {
184-
return "", errors.New("can't scrape the version info page, HTML structure may have changed")
185-
}
186-
return html.UnescapeString(hrefMatches[1]), nil
187-
}

0 commit comments

Comments
 (0)