Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug 1928350 - Let macOS 10.12 to 10.14 clients download ESR 115 #397

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docker/initdb.d/02-data.sql
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ INSERT INTO `mirror_aliases` (`id`, `alias`, `related_product`) VALUES (9,'firef
INSERT INTO `mirror_aliases` (`id`, `alias`, `related_product`) VALUES (10,'firefox-devedition-stub','Firefox-stub');
INSERT INTO `mirror_aliases` (`id`, `alias`, `related_product`) VALUES (11,'firefox-devedition-msi-latest-ssl','Firefox-beta-msi-latest-SSL');
INSERT INTO `mirror_aliases` (`id`, `alias`, `related_product`) VALUES (12,'thunderbird-latest-ssl','Thunderbird-SSL');
INSERT INTO `mirror_aliases` (`id`, `alias`, `related_product`) VALUES (13,'firefox-esr115-pkg-latest-ssl','Firefox-115.16.1esr-pkg-SSL');
/*!40000 ALTER TABLE `mirror_aliases` ENABLE KEYS */;
UNLOCK TABLES;

Expand Down Expand Up @@ -122,6 +123,9 @@ INSERT INTO `mirror_locations` (`path`, `product_id`, `os_id`, `id`) VALUES ('/f
INSERT INTO `mirror_locations` (`path`, `product_id`, `os_id`, `id`) VALUES ('/thunderbird/releases/131.0.1/win64/:lang/Thunderbird%20Setup%20131.0.1.exe',28,1,67);
INSERT INTO `mirror_locations` (`path`, `product_id`, `os_id`, `id`) VALUES ('/thunderbird/releases/131.0.1/win32/:lang/Thunderbird%20Setup%20131.0.1.exe',28,3,68);

INSERT INTO `mirror_locations` (`path`, `product_id`, `os_id`, `id`) VALUES ('/firefox/releases/115.16.1esr/mac/:lang/Firefox%20115.16.1esr.dmg',20,2,69);
INSERT INTO `mirror_locations` (`path`, `product_id`, `os_id`, `id`) VALUES ('/firefox/releases/115.16.1esr/mac/:lang/Firefox%20115.16.1esr.pkg',29,2,70);

/*!40000 ALTER TABLE `mirror_locations` ENABLE KEYS */;
UNLOCK TABLES;

Expand Down Expand Up @@ -174,6 +178,7 @@ INSERT INTO `mirror_products` (`count`, `name`, `checknow`, `priority`, `active`
INSERT INTO `mirror_products` (`count`, `name`, `checknow`, `priority`, `active`, `id`, `ssl_only`) VALUES (0,'Firefox-nightly-msi-latest-SSL',1,1,1,26,1);
INSERT INTO `mirror_products` (`count`, `name`, `checknow`, `priority`, `active`, `id`, `ssl_only`) VALUES (0,'Firefox-esr-msi-latest-SSL',1,1,1,27,1);
INSERT INTO `mirror_products` (`count`, `name`, `checknow`, `priority`, `active`, `id`, `ssl_only`) VALUES (0,'Thunderbird-SSL',1,1,1,28,1);
INSERT INTO `mirror_products` (`count`, `name`, `checknow`, `priority`, `active`, `id`, `ssl_only`) VALUES (0,'Firefox-115.16.1esr-pkg-SSL',1,1,1,29,1);
/*!40000 ALTER TABLE `mirror_products` ENABLE KEYS */;
UNLOCK TABLES;

Expand Down
219 changes: 41 additions & 178 deletions handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,26 @@ import (
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
"time"
"unicode"

"github.com/mozilla-services/go-bouncer/bouncer"
)

const (
DefaultLang = "en-US"
DefaultOS = "win"
firefoxSHA1ESRAliasSuffix = "sha1"
fxPre2024LastNightly = "firefox-nightly-pre2024"
fxPre2024LastBeta = "127.0b9"
fxPre2024LastRelease = "127.0"
esr115Product = "firefox-esr115-latest-ssl"
defaultLang = "en-US"
defaultOS = "win"
fxPre2024LastNightly = "firefox-nightly-pre2024"
fxPre2024LastBeta = "127.0b9"
fxPre2024LastRelease = "127.0"
esr115Product = "firefox-esr115-latest-ssl"
)

type xpRelease struct {
Version string
}

var (
// detects Windows XP and Vista clients
windowsXPRegex = regexp.MustCompile(`Windows (?:NT 5\.1|XP|NT 5\.2|NT 6\.0)`)
// detects windows 7/8/8.1 clients
windowsRegexForESR115 = regexp.MustCompile(`Windows (?:NT 6\.(1|2|3))`)
// this is used to verify the referer header
Expand All @@ -42,17 +37,13 @@ var (
fxPartnerAlias = regexp.MustCompile(`^partner-firefox-release-([^-]*)-(.*)-latest$`)
// detects x64 clients
win64Regex = regexp.MustCompile(`Win64|WOW64`)
// detects macOS 10.12 to 10.14 clients. Note that we can't keep doing UA-based
// detection forever... https://bugzilla.mozilla.org/show_bug.cgi?id=1679929
oldMacOSRegex = regexp.MustCompile(`Macintosh; Intel Mac OS X 10[\._]1(2|3|4)`)
)

var tBirdWinXPLastRelease = xpRelease{"38.5.0"}
var tBirdWinXPLastBeta = xpRelease{"43.0b1"}

func isWindowsXPUserAgent(userAgent string) bool {
return windowsXPRegex.MatchString(userAgent)
}

func isUserAgentOnlyCompatibleWithESR115(userAgent string) bool {
return windowsRegexForESR115.MatchString(userAgent)
return windowsRegexForESR115.MatchString(userAgent) || oldMacOSRegex.MatchString(userAgent)
}

func hasMozorgReferrer(referrer string) bool {
Expand All @@ -63,133 +54,8 @@ func isWin64UserAgent(userAgent string) bool {
return win64Regex.MatchString(userAgent)
}

func isNotNumber(r rune) bool {
return !unicode.IsNumber(r)
}

// a < b = -1
// a == b = 0
// a > b = 1
func compareVersions(a, b string) int {
if a == b {
return 0
}
aParts := strings.Split(a, ".")
bParts := strings.Split(b, ".")

for i, verA := range aParts {
if len(bParts) <= i {
return 1
}
verB := bParts[i]

aInt, err := strconv.Atoi(strings.TrimRightFunc(verA, isNotNumber))
if err != nil {
aInt = 0
}
bInt, err := strconv.Atoi(strings.TrimRightFunc(verB, isNotNumber))
if err != nil {
bInt = 0
}

if aInt > bInt {
return 1
}
if aInt < bInt {
return -1
}
}
return 0
}

func tBirdSha1Product(productSuffix string) string {
switch productSuffix {
case "beta", "beta-latest":
return tBirdWinXPLastBeta.Version
case "ssl":
return tBirdWinXPLastRelease.Version + "-ssl"
case "latest":
return tBirdWinXPLastRelease.Version
}

productSuffixParts := strings.SplitN(productSuffix, "-", 2)
ver := productSuffixParts[0]

possibleVersion := tBirdWinXPLastRelease
if strings.Contains(ver, ".0b") {
possibleVersion = tBirdWinXPLastBeta
}

if compareVersions(ver, possibleVersion.Version) == -1 {
return productSuffix
}

if len(productSuffixParts) == 1 {
return possibleVersion.Version
}

if productSuffixParts[1] == "ssl" {
return possibleVersion.Version + "-ssl"
}

return productSuffix
}

func firefoxSha1Product(productSuffix string) string {
// Example list of products:
// Firefox-48.0-Complete
// Firefox-48.0build1-Complete
// Firefox-48.0
// Firefox-48.0-SSL
// Firefox-48.0-stub
// Firefox-48.0build1-Partial-47.0build3
// Firefox-48.0build1-Partial-47.0.1build1
// Firefox-48.0build1-Partial-48.0b10build1
// Firefox-48.0-Partial-47.0
// Firefox-48.0-Partial-47.0.1
// Firefox-48.0-Partial-48.0b10

// Example list of aliases:
// firefox-beta-latest
// firefox-beta-sha1
// Firefox-beta-stub
// firefox-esr-latest
// firefox-esr-sha1
// firefox-latest
// firefox-sha1
// Firefox-stub

// Do not touch products ending with "sha1"
if strings.HasSuffix(productSuffix, "-sha1") {
return productSuffix
}

// Do not touch completes and partials
if strings.HasSuffix(productSuffix, "-complete") || strings.Contains(productSuffix, "-partial-") {
return productSuffix
}
return firefoxSHA1ESRAliasSuffix
}

func sha1Product(product string) string {
productParts := strings.SplitN(product, "-", 2)
if len(productParts) == 1 {
return product
}

if productParts[0] == "firefox" {
return "firefox-" + firefoxSha1Product(productParts[1])
}

if productParts[0] == "thunderbird" {
return "thunderbird-" + tBirdSha1Product(productParts[1])
}

return product
}

// detect stub installers that pin the "DigiCert SHA2 Assured ID Code Signing CA" intermediate

// isPre2024StubUserAgent is used to detect stub installers that pin the
// "DigiCert SHA2 Assured ID Code Signing CA" intermediate.
func isPre2024StubUserAgent(userAgent string) bool {
return "NSIS InetBgDL (Mozilla)" == userAgent
}
Expand Down Expand Up @@ -272,7 +138,7 @@ func (h *HealthHandler) check() *HealthResult {
return result
}

func (h *HealthHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
func (h *HealthHandler) ServeHTTP(w http.ResponseWriter, _ *http.Request) {
if h.CacheTime > 0 {
w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%d", h.CacheTime/time.Second))
}
Expand All @@ -291,15 +157,15 @@ type BouncerHandler struct {
db *bouncer.DB

CacheTime time.Duration
PinHttpsHeaderName string
PinHTTPSHeaderName string
PinnedBaseURLHttp string
PinnedBaseURLHttps string
StubRootURL string
}

// URL returns the final redirect URL given a lang, os and product
// if the string is == "", no mirror or location was found
func (b *BouncerHandler) URL(pinHttps bool, lang, os, product string) (string, error) {
func (b *BouncerHandler) URL(pinHTTPS bool, lang, os, product string) (string, error) {
product, err := b.db.AliasFor(product)
if err != nil {
return "", err
Expand Down Expand Up @@ -331,7 +197,7 @@ func (b *BouncerHandler) URL(pinHttps bool, lang, os, product string) (string, e
locationPath = strings.Replace(locationPath, ":lang", lang, -1)

mirrorBaseURL := "http://" + b.PinnedBaseURLHttp
if pinHttps || sslOnly {
if pinHTTPS || sslOnly {
mirrorBaseURL = "https://" + b.PinnedBaseURLHttps
}

Expand All @@ -349,27 +215,27 @@ func (b *BouncerHandler) stubAttributionURL(reqParams *BouncerParams) string {
return b.StubRootURL + "?" + query.Encode()
}

func (b *BouncerHandler) shouldPinHttps(req *http.Request) bool {
if b.PinHttpsHeaderName == "" {
func (b *BouncerHandler) shouldPinHTTPS(req *http.Request) bool {
if b.PinHTTPSHeaderName == "" {
return false
}

return req.Header.Get(b.PinHttpsHeaderName) == "https"
return req.Header.Get(b.PinHTTPSHeaderName) == "https"
}

func fromRTAMO(attribution_code string) bool {
func fromRTAMO(attributionCode string) bool {
// base64 decode the attribution_code value to see if it matches the RTAMO regex
// This uses '.' as padding because Bedrock is using this library to encode the values:
// https://pypi.org/project/querystringsafe-base64/
var base64Decoder = base64.URLEncoding.WithPadding('.')
sDec, err := base64Decoder.DecodeString(attribution_code)
sDec, err := base64Decoder.DecodeString(attributionCode)
if err != nil {
log.Printf("Error decoding %s: %s ", attribution_code, err.Error())
log.Printf("Error decoding %s: %s ", attributionCode, err.Error())
return false
}
q, err := url.ParseQuery(string(sDec))
if err != nil {
log.Printf("Error parsing the attribution_code query parameters: %s", err.Error())
log.Printf("Error parsing the attribution_code query parameter: %s", err.Error())
return false
}

Expand Down Expand Up @@ -439,40 +305,36 @@ func (b *BouncerHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
}

if reqParams.OS == "" {
reqParams.OS = DefaultOS
reqParams.OS = defaultOS
}

if reqParams.Lang == "" {
reqParams.Lang = DefaultLang
reqParams.Lang = defaultLang
}

// If attribution_code is set, redirect to the stub service.
if b.shouldAttribute(reqParams) {
stubURL := b.stubAttributionURL(reqParams)
http.Redirect(w, req, stubURL, 302)
return
}

isWinXpClient := isWindowsXPUserAgent(req.UserAgent())
isPre2024Stub := isPre2024StubUserAgent(req.UserAgent())
// We want to return ESR115 when... the product is for Firefox
shouldReturnESR115 := strings.HasPrefix(reqParams.Product, "firefox-") &&
// and the product is _not_ an MSI build
!strings.Contains(reqParams.Product, "-msi") &&
// and the product is _not_ a (macOS) pkg build
!strings.Contains(reqParams.Product, "-pkg") &&
// and the product is _not_ a partial or complete update (MAR files)
!strings.Contains(reqParams.Product, "-partial") &&
!strings.Contains(reqParams.Product, "-complete") &&
// and the OS param specifies windows
strings.HasPrefix(reqParams.OS, "win") &&
// and the User-Agent says it's a Windows 7/8/8.1 client
// and the OS param specifies windows or macOS
(strings.HasPrefix(reqParams.OS, "win") || strings.HasPrefix(reqParams.OS, "osx")) &&
// and the User-Agent says it's a Windows 7/8/8.1 or macOS 10.12/10.13/10.14 client
isUserAgentOnlyCompatibleWithESR115(req.UserAgent()) &&
// and the request doesn't come from mozilla.org
!hasMozorgReferrer(reqParams.Referer)

// If the client is not WinXP and attribution_code is set, redirect to the stub service
if b.shouldAttribute(reqParams) && !isWinXpClient {
stubURL := b.stubAttributionURL(reqParams)
http.Redirect(w, req, stubURL, 302)
return
}

// HACKS
// If the user is coming from windows xp or vista, send a sha1 signed product.
if reqParams.OS == "win" && isWinXpClient {
reqParams.Product = sha1Product(reqParams.Product)
}
// Send the latest compatible ESR product if we detect that this is the best option for the client.
if shouldReturnESR115 {
// Override the OS if we detect a x64 client that attempts to get a stub installer.
Expand All @@ -481,12 +343,13 @@ func (b *BouncerHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
}
reqParams.Product = esr115Product
}

// If the user is an "old" stub installer, send a pre-2024-cert-rotation product.
if isPre2024Stub {
if isPre2024StubUserAgent(req.UserAgent()) {
reqParams.Product = pre2024Product(reqParams.Product)
}

url, err := b.URL(b.shouldPinHttps(req), reqParams.Lang, reqParams.OS, reqParams.Product)
url, err := b.URL(b.shouldPinHTTPS(req), reqParams.Lang, reqParams.OS, reqParams.Product)
if err != nil {
http.Error(w, "Internal Server Error.", http.StatusInternalServerError)
log.Println(err)
Expand Down
Loading