diff --git a/docker/initdb.d/02-data.sql b/docker/initdb.d/02-data.sql index 7747063..c9abfc2 100644 --- a/docker/initdb.d/02-data.sql +++ b/docker/initdb.d/02-data.sql @@ -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; @@ -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; @@ -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; diff --git a/handlers.go b/handlers.go index 535fdeb..eff7917 100644 --- a/handlers.go +++ b/handlers.go @@ -9,22 +9,19 @@ 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 { @@ -32,8 +29,6 @@ type xpRelease struct { } 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 @@ -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 { @@ -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 } @@ -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)) } @@ -291,7 +157,7 @@ type BouncerHandler struct { db *bouncer.DB CacheTime time.Duration - PinHttpsHeaderName string + PinHTTPSHeaderName string PinnedBaseURLHttp string PinnedBaseURLHttps string StubRootURL string @@ -299,7 +165,7 @@ type BouncerHandler struct { // 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 @@ -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 } @@ -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 } @@ -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. @@ -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) diff --git a/handlers_test.go b/handlers_test.go index c8926d6..f45923d 100644 --- a/handlers_test.go +++ b/handlers_test.go @@ -26,7 +26,7 @@ func init() { bouncerHandler = &BouncerHandler{ db: testDB, StubRootURL: "https://stub/", - PinHttpsHeaderName: "X-Forwarded-Proto", + PinHTTPSHeaderName: "X-Forwarded-Proto", PinnedBaseURLHttp: "download.cdn.mozilla.net/pub", PinnedBaseURLHttps: "download-installer.cdn.mozilla.net/pub", } @@ -34,7 +34,7 @@ func init() { db: testDB, PinnedBaseURLHttp: "download-sha1.cdn.mozilla.net/pub", PinnedBaseURLHttps: "download-sha1.cdn.mozilla.net/pub", - PinHttpsHeaderName: "X-Forwarded-Proto", + PinHTTPSHeaderName: "X-Forwarded-Proto", } } @@ -217,23 +217,23 @@ func TestBouncerHandlerParams(t *testing.T) { } func TestBouncerShouldPinHttps(t *testing.T) { - bouncerHandler.PinHttpsHeaderName = "" + bouncerHandler.PinHTTPSHeaderName = "" req, err := http.NewRequest("GET", "http://test/?product=firefox-latest&os=osx&lang=en-US", nil) assert.NoError(t, err) - assert.Equal(t, false, bouncerHandler.shouldPinHttps(req)) + assert.Equal(t, false, bouncerHandler.shouldPinHTTPS(req)) req.Header.Set("X-Forwarded-Proto", "https") - assert.Equal(t, false, bouncerHandler.shouldPinHttps(req)) + assert.Equal(t, false, bouncerHandler.shouldPinHTTPS(req)) - bouncerHandler.PinHttpsHeaderName = "X-Forwarded-Proto" + bouncerHandler.PinHTTPSHeaderName = "X-Forwarded-Proto" - assert.Equal(t, true, bouncerHandler.shouldPinHttps(req)) + assert.Equal(t, true, bouncerHandler.shouldPinHTTPS(req)) req.Header.Set("X-Forwarded-Proto", "http") - assert.Equal(t, false, bouncerHandler.shouldPinHttps(req)) + assert.Equal(t, false, bouncerHandler.shouldPinHTTPS(req)) req.Header.Del("X-Forwarded-Proto") - assert.Equal(t, false, bouncerHandler.shouldPinHttps(req)) + assert.Equal(t, false, bouncerHandler.shouldPinHTTPS(req)) } func TestBouncerHandlerPrintQuery(t *testing.T) { @@ -247,38 +247,6 @@ func TestBouncerHandlerPrintQuery(t *testing.T) { assert.Equal(t, "http://download.cdn.mozilla.net/pub/firefox/releases/39.0/mac/en-US/Firefox%2039.0.dmg", w.Body.String()) } -func TestBouncerHandlerPinnedValid(t *testing.T) { - defaultUA := "Mozilla/5.0 (Windows NT 7.0; rv:10.0) Gecko/20100101 Firefox/43.0" - testRequests := []struct { - URL string - ExpectedLocation string - UserAgent string - XForwardedProto string - }{ - {"http://test/?product=firefox-latest&os=osx&lang=en-US", "http://download-sha1.cdn.mozilla.net/pub/firefox/releases/39.0/mac/en-US/Firefox%2039.0.dmg", defaultUA, "http"}, - {"http://test/?product=firefox-latest&os=win64&lang=en-US", "http://download-sha1.cdn.mozilla.net/pub/firefox/releases/39.0/win64/en-US/Firefox%20Setup%2039.0.exe", defaultUA, "http"}, - {"http://test/?product=firefox-latest&os=win64&lang=en-US", "https://download-sha1.cdn.mozilla.net/pub/firefox/releases/39.0/win64/en-US/Firefox%20Setup%2039.0.exe", defaultUA, "https"}, - {"http://test/?product=Firefox-SSL&os=win64&lang=en-US", "https://download-sha1.cdn.mozilla.net/pub/firefox/releases/39.0/win64/en-US/Firefox%20Setup%2039.0.exe", defaultUA, "http"}, - {"http://test/?product=Firefox-SSL&os=win&lang=en-US", "https://download-sha1.cdn.mozilla.net/pub/firefox/releases/43.0.1/win32/en-US/Firefox%20Setup%2043.0.1.exe", "Mozilla/5.0 (Windows; U; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727)", "http"}, // Windows XP - {"http://test/?product=Firefox-SSL&os=win&lang=en-US", "https://download-sha1.cdn.mozilla.net/pub/firefox/releases/43.0.1/win32/en-US/Firefox%20Setup%2043.0.1.exe", "Mozilla/5.0 (Windows; U; MSIE 6.0; Windows NT 6.0; SV1; .NET CLR 2.0.50727)", "http"}, // Windows Vista - {"http://test/?product=Firefox-SSL&os=win64&lang=en-US", "https://download-sha1.cdn.mozilla.net/pub/firefox/releases/39.0/win64/en-US/Firefox%20Setup%2039.0.exe", "Mozilla/5.0 (Windows; U; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727)", "http"}, // Windows XP 64 bit - should get normal win64 build - {"http://test/?product=Firefox-stub&os=win&lang=en-US", "https://download-sha1.cdn.mozilla.net/pub/firefox/releases/43.0.1/win32/en-US/Firefox%20Setup%2043.0.1.exe", "Mozilla/5.0 (Windows; U; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727)", "http"}, // Windows XP no stub - } - - for _, testRequest := range testRequests { - w := httptest.NewRecorder() - req, err := http.NewRequest("GET", testRequest.URL, nil) - req.Header.Set("X-Forwarded-Proto", testRequest.XForwardedProto) - assert.NoError(t, err, "url: %v ua: %v", testRequest.URL, testRequest.UserAgent) - - req.Header.Set("User-Agent", testRequest.UserAgent) - - bouncerHandlerPinned.ServeHTTP(w, req) - assert.Equal(t, 302, w.Code, "url: %v ua: %v", testRequest.URL, testRequest.UserAgent) - assert.Equal(t, testRequest.ExpectedLocation, w.HeaderMap.Get("Location"), "url: %v ua: %v", testRequest.URL, testRequest.UserAgent) - } -} - func TestBouncerHandlerValid(t *testing.T) { defaultUA := "Mozilla/5.0 (Windows NT 7.0; rv:10.0) Gecko/20100101 Firefox/43.0" testRequests := []struct { @@ -289,10 +257,6 @@ func TestBouncerHandlerValid(t *testing.T) { {"http://test/?product=firefox-latest&os=osx&lang=en-US", "http://download.cdn.mozilla.net/pub/firefox/releases/39.0/mac/en-US/Firefox%2039.0.dmg", defaultUA}, {"http://test/?product=firefox-latest&os=win64&lang=en-US", "http://download.cdn.mozilla.net/pub/firefox/releases/39.0/win64/en-US/Firefox%20Setup%2039.0.exe", defaultUA}, {"http://test/?product=Firefox-SSL&os=win64&lang=en-US", "https://download-installer.cdn.mozilla.net/pub/firefox/releases/39.0/win64/en-US/Firefox%20Setup%2039.0.exe", defaultUA}, - {"http://test/?product=Firefox-SSL&os=win&lang=en-US", "https://download-installer.cdn.mozilla.net/pub/firefox/releases/43.0.1/win32/en-US/Firefox%20Setup%2043.0.1.exe", "Mozilla/5.0 (Windows; U; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727)"}, // Windows XP - {"http://test/?product=Firefox-SSL&os=win&lang=en-US", "https://download-installer.cdn.mozilla.net/pub/firefox/releases/43.0.1/win32/en-US/Firefox%20Setup%2043.0.1.exe", "Mozilla/5.0 (Windows; U; MSIE 6.0; Windows NT 6.0; SV1; .NET CLR 2.0.50727)"}, // Windows Vista - {"http://test/?product=Firefox-SSL&os=win64&lang=en-US", "https://download-installer.cdn.mozilla.net/pub/firefox/releases/39.0/win64/en-US/Firefox%20Setup%2039.0.exe", "Mozilla/5.0 (Windows; U; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727)"}, // Windows XP 64 bit - should get normal win64 build - {"http://test/?product=Firefox-stub&os=win&lang=en-US", "https://download-installer.cdn.mozilla.net/pub/firefox/releases/43.0.1/win32/en-US/Firefox%20Setup%2043.0.1.exe", "Mozilla/5.0 (Windows; U; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727)"}, // Windows XP no stub {"http://test/?product=Firefox-nightly-latest-ssl&os=win&lang=en-US", "https://download-installer.cdn.mozilla.net/pub/firefox/nightly/2024/05/2024-05-06-09-48-55-mozilla-central-l10n/firefox-127.0a1.en-US.win32.installer.exe", "NSIS InetBgDL (Mozilla)"}, // old stub {"http://test/?product=Firefox-nightly-latest-ssl&os=win&lang=en-US", "https://download-installer.cdn.mozilla.net/pub/firefox/nightly/latest-mozilla-central/firefox-128.0a1.en-US.win32.installer.exe", "NSIS InetBgDL (Mozilla 2024)"}, // new stub {"http://test/?product=Firefox-nightly-latest-ssl&os=win&lang=en-US", "https://download-installer.cdn.mozilla.net/pub/firefox/nightly/latest-mozilla-central/firefox-128.0a1.en-US.win32.installer.exe", defaultUA}, @@ -357,133 +321,50 @@ func TestBouncerHandlerPre2024(t *testing.T) { } } -func TestIsWindowsXPUserAgent(t *testing.T) { - uas := []struct { - UA string - IsXP bool - }{ - {"Mozilla/5.0 (Windows NT 5.1; rv:31.0) Gecko/20100101 Firefox/31.0", true}, // firefox XP - {"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:31.0) Gecko/20130401 Firefox/31.0", false}, // firefox non-XP - {"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2224.3 Safari/537.36", true}, // Chrome XP - {"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2225.0 Safari/537.36", false}, // Chrome non-XP - {"Mozilla/5.0 (Windows; U; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727)", true}, // IE XP - {"Mozilla/4.0 (compatible; MSIE 6.1; Windows XP)", true}, // IE XP - {"Mozilla/5.0 (Windows; U; MSIE 7.0; Windows NT 6.0; en-US)", true}, // IE Vista - {"Mozilla/5.0 (Windows; U; MSIE 7.0; Windows NT 6.1; en-US)", false}, // IE non-XP - } - for _, ua := range uas { - assert.Equal(t, ua.IsXP, isWindowsXPUserAgent(ua.UA), "ua: %v", ua.UA) - } -} - -func TestSha1Product(t *testing.T) { - // Ignore products ending with sha1 - assert.Equal(t, "firefox-something-sha1", sha1Product("firefox-something-sha1")) - assert.Equal(t, "firefox-45.0-sha1", sha1Product("firefox-45.0-sha1")) - assert.Equal(t, "firefox-45.0.2-sha1", sha1Product("firefox-45.0.2-sha1")) - assert.Equal(t, "firefox-49.0b1-sha1", sha1Product("firefox-49.0b1-sha1")) - assert.Equal(t, "firefox-49.0b2-sha1", sha1Product("firefox-49.0b2-sha1")) - assert.Equal(t, "firefox-45.0esr-sha1", sha1Product("firefox-45.0esr-sha1")) - assert.Equal(t, "firefox-45.0.2esr-sha1", sha1Product("firefox-45.0.2esr-sha1")) - assert.Equal(t, "firefox-45.1.0esr-sha1", sha1Product("firefox-45.1.0esr-sha1")) - assert.Equal(t, "firefox-45.1.2esr-sha1", sha1Product("firefox-45.1.2esr-sha1")) - - // Ignore partials and completes - assert.Equal(t, "firefox-42.0.0-complete", sha1Product("firefox-42.0.0-complete")) - assert.Equal(t, "firefox-48.0-partial-41.0.2build1", sha1Product("firefox-48.0-partial-41.0.2build1")) - assert.Equal(t, "firefox-43.0.2-complete", sha1Product("firefox-43.0.2-complete")) - assert.Equal(t, "firefox-44.0-complete", sha1Product("firefox-44.0-complete")) - assert.Equal(t, "firefox-45.0b1-complete", sha1Product("firefox-45.0b1-complete")) - assert.Equal(t, "firefox-48.0-partial-42.0b1", sha1Product("firefox-48.0-partial-42.0b1")) - assert.Equal(t, "firefox-48.0b9-partial-48.0b1", sha1Product("firefox-48.0b9-partial-48.0b1")) - - // ignore product wihtout dashes - assert.Equal(t, "firefox", sha1Product("firefox")) - - // Aliases with no version specified - assert.Equal(t, "firefox-sha1", sha1Product("firefox-latest")) - assert.Equal(t, "firefox-sha1", sha1Product("firefox-stub")) - assert.Equal(t, "firefox-sha1", sha1Product("firefox-beta-latest")) - assert.Equal(t, "firefox-sha1", sha1Product("firefox-beta-stub")) - assert.Equal(t, "firefox-sha1", sha1Product("firefox-esr-latest")) - assert.Equal(t, "firefox-sha1", sha1Product("firefox-esr-stub")) - - // Aurora is special a bit - assert.Equal(t, "firefox-sha1", sha1Product("firefox-aurora")) - assert.Equal(t, "firefox-sha1", sha1Product("firefox-aurora-stub")) - - // Beta versions - assert.Equal(t, "firefox-sha1", sha1Product("firefox-48.0b1")) - assert.Equal(t, "firefox-sha1", sha1Product("firefox-49.0b8")) - assert.Equal(t, "firefox-sha1", sha1Product("firefox-48.0b1-stub")) - assert.Equal(t, "firefox-sha1", sha1Product("firefox-49.0b8-stub")) - assert.Equal(t, "firefox-sha1", sha1Product("firefox-48.0b1-ssl")) - assert.Equal(t, "firefox-sha1", sha1Product("firefox-49.0b8-ssl")) - - // ESR - assert.Equal(t, "firefox-sha1", sha1Product("firefox-45.0esr")) - assert.Equal(t, "firefox-sha1", sha1Product("firefox-45.0.1esr")) - assert.Equal(t, "firefox-sha1", sha1Product("firefox-45.3.0esr")) - assert.Equal(t, "firefox-sha1", sha1Product("firefox-45.3.1esr")) - assert.Equal(t, "firefox-sha1", sha1Product("firefox-45.0esr-stub")) - assert.Equal(t, "firefox-sha1", sha1Product("firefox-45.0.1esr-stub")) - assert.Equal(t, "firefox-sha1", sha1Product("firefox-45.3.0esr-stub")) - assert.Equal(t, "firefox-sha1", sha1Product("firefox-45.3.1esr-stub")) - assert.Equal(t, "firefox-sha1", sha1Product("firefox-45.0esr-ssl")) - assert.Equal(t, "firefox-sha1", sha1Product("firefox-45.0.1esr-ssl")) - assert.Equal(t, "firefox-sha1", sha1Product("firefox-45.3.0esr-ssl")) - assert.Equal(t, "firefox-sha1", sha1Product("firefox-45.3.1esr-ssl")) - - // Everything else starting with firefox should go to firefox-sha1 - assert.Equal(t, "firefox-sha1", sha1Product("firefox-42.0.0")) - assert.Equal(t, "firefox-sha1", sha1Product("firefox-48.0")) - assert.Equal(t, "firefox-sha1", sha1Product("firefox-48.0.1")) - assert.Equal(t, "firefox-sha1", sha1Product("firefox-8.0")) - assert.Equal(t, "firefox-sha1", sha1Product("firefox-8.0.4")) - assert.Equal(t, "firefox-sha1", sha1Product("firefox-42.0.0-stub")) - assert.Equal(t, "firefox-sha1", sha1Product("firefox-48.0-stub")) - assert.Equal(t, "firefox-sha1", sha1Product("firefox-48.0.1-stub")) - assert.Equal(t, "firefox-sha1", sha1Product("firefox-8.0-stub")) - assert.Equal(t, "firefox-sha1", sha1Product("firefox-8.0.4-stub")) - assert.Equal(t, "firefox-sha1", sha1Product("firefox-42.0.0-ssl")) - assert.Equal(t, "firefox-sha1", sha1Product("firefox-48.0-ssl")) - assert.Equal(t, "firefox-sha1", sha1Product("firefox-48.0.1-ssl")) - assert.Equal(t, "firefox-sha1", sha1Product("firefox-8.0-ssl")) - assert.Equal(t, "firefox-sha1", sha1Product("firefox-8.0.4-ssl")) - assert.Equal(t, "firefox-sha1", sha1Product("firefox-42.0.0-something-new")) - assert.Equal(t, "firefox-sha1", sha1Product("firefox-48.0-ssl-something-new")) - assert.Equal(t, "firefox-sha1", sha1Product("firefox-48.0.1-ssl-something-new")) - assert.Equal(t, "firefox-sha1", sha1Product("firefox-8.0-something-old")) - assert.Equal(t, "firefox-sha1", sha1Product("firefox-8.0.4-ssl-something-old")) - - assert.Equal(t, "thunderbird-38.5.0", sha1Product("thunderbird-38.6.0")) - assert.Equal(t, "thunderbird-38.5.0", sha1Product("thunderbird-39.0.0")) - - assert.Equal(t, "thunderbird-38.4.0", sha1Product("thunderbird-38.4.0")) - - assert.Equal(t, "thunderbird-43.0b1", sha1Product("thunderbird-43.0b2")) - assert.Equal(t, "thunderbird-43.0b1", sha1Product("thunderbird-44.0b1")) - - assert.Equal(t, "thunderbird-42.0b1", sha1Product("thunderbird-42.0b1")) -} - func TestIsUserAgentOnlyCompatibleWithESR115(t *testing.T) { uas := []struct { - UA string - IsWin7 bool + UA string + IsCompatible bool }{ - {"Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko", true}, // IE 64bits Win7 - {"Opera/9.80 (Windows NT 6.1; U; en) Presto/2.7.62 Version/11.01", true}, // Opera 11 Win7 - {"Mozilla/5.0 (Windows; U; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727)", false}, // IE XP - {"Mozilla/5.0 (Windows; U; MSIE 7.0; Windows NT 6.0; en-US)", false}, // IE Vista - {"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36 Edg/100.0.1185.36", true}, // Edge Win7 - {"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.71 Safari/537.36", true}, // Chrome Win8 - {"Mozilla/5.0 (Windows NT 6.3; WOW64; rv:124.0) Gecko/20100101 Firefox/124.0", true}, // Firefox Win8.1 - {"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36", false}, // Safari Win10 - {"Mozilla/5.0 (Windows NT 611; WOW64; Trident/7.0; rv:11.0) like Gecko", false}, // Bogus + // IE 64bits Win7 + {"Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko", true}, + // Opera 11 Win7 + {"Opera/9.80 (Windows NT 6.1; U; en) Presto/2.7.62 Version/11.01", true}, + // IE XP + {"Mozilla/5.0 (Windows; U; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727)", false}, + // IE Vista + {"Mozilla/5.0 (Windows; U; MSIE 7.0; Windows NT 6.0; en-US)", false}, + // Edge Win7 + {"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36 Edg/100.0.1185.36", true}, + // Chrome Win8 + {"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.71 Safari/537.36", true}, + // Firefox Win8.1 + {"Mozilla/5.0 (Windows NT 6.3; WOW64; rv:124.0) Gecko/20100101 Firefox/124.0", true}, + // Safari Win10 + {"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36", false}, + // Bogus + {"Mozilla/5.0 (Windows NT 611; WOW64; Trident/7.0; rv:11.0) like Gecko", false}, + // macOS versions < 10.12 + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/601.7.8 (KHTML, like Gecko) Version/9.1.3 Safari/537.86.7", false}, + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36", false}, + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9) AppleWebKit/537.71 (KHTML, like Gecko) Version/7.0 Safari/537.71", false}, + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:47.0) Gecko/20100101 Firefox/47.0", false}, + // macOS 10.12 + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:109.0) Gecko/20100101 Firefox/115.0", true}, + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36", true}, + // macOS 10.13 + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Safari/605.1.15", true}, + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:109.0) Gecko/20100101 Firefox/115.0", true}, + // macOS 10.14 + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Safari/605.1.15. As: Safari 12 on macOS (Mojave).", true}, + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:68.0) Gecko/20100101 Firefox/68.0", true}, + // Firefox on iPhone + {"Mozilla/5.0 (iPhone; CPU iPhone OS 12_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/18.2b15817 Mobile/15E148 Safari/605.1.15", false}, + // macOS 10.15 + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.1 Safari/605.1.15", false}, } for _, ua := range uas { - assert.Equal(t, ua.IsWin7, isUserAgentOnlyCompatibleWithESR115(ua.UA), "ua: %v", ua.UA) + assert.Equal(t, ua.IsCompatible, isUserAgentOnlyCompatibleWithESR115(ua.UA), "ua: %v", ua.UA) } } @@ -632,13 +513,6 @@ func TestBouncerHandlerForWindowsOnlyCompatibleWithESR115WithMozorgReferrer(t *t assert.Equal(t, expectedLocation, w.HeaderMap.Get("Location")) } -func BenchmarkSha1Product(b *testing.B) { - for i := 0; i < b.N; i++ { - sha1Product("firefox-43.0.0") - sha1Product("firefox-44.0b1") - } -} - func TestHealthHandler(t *testing.T) { testDB, err := bouncer.NewDB(testDSN) if err != nil { @@ -658,3 +532,80 @@ func TestHealthHandler(t *testing.T) { assert.Equal(t, 200, w.Code) assert.Equal(t, fmt.Sprintf(`{"db":true,"healthy":true,"version":"%s"}`, bouncer.Version), w.Body.String()) } + +func TestBouncerHandlerForMacOSOnlyCompatibleWithESR115(t *testing.T) { + for _, tc := range []struct { + userAgent string + }{ + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:109.0) Gecko/20100101 Firefox/115.0"}, + } { + for _, url := range []string{ + "http://test/?product=firefox-beta&os=osx&lang=en-US", + "http://test/?product=firefox-devedition&os=osx&lang=en-US", + "http://test/?product=firefox-nightly-latest-ssl&os=osx&lang=en-US", + "http://test/?product=firefox-ssl-latest&os=osx&lang=en-US", + "http://test/?product=firefox-unknown&os=osx&lang=en-US", + } { + expectedLocation := "//download-installer.cdn.mozilla.net/pub/firefox/releases/115.16.1esr/mac/en-US/Firefox%20115.16.1esr.dmg" + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", url, nil) + req.Header.Set("User-Agent", tc.userAgent) + + bouncerHandler.ServeHTTP(w, req) + + assert.Equal(t, 302, w.Code, "userAgent: %v, url: %v", tc.userAgent, url) + // We don't need to assert the scheme. + assert.True(t, strings.HasSuffix(w.HeaderMap.Get("Location"), expectedLocation), "userAgent: %v, url: %v", tc.userAgent, url) + } + + // -pkg products + for _, url := range []string{ + "http://test/?product=firefox-esr115-pkg-latest-ssl&os=osx&lang=en-US", + "http://test/?product=firefox-115.16.1esr-pkg-ssl&os=osx&lang=en-US", + } { + expectedLocation := "//download-installer.cdn.mozilla.net/pub/firefox/releases/115.16.1esr/mac/en-US/Firefox%20115.16.1esr.pkg" + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", url, nil) + req.Header.Set("User-Agent", tc.userAgent) + + bouncerHandler.ServeHTTP(w, req) + + assert.Equal(t, 302, w.Code, "userAgent: %v, url: %v", tc.userAgent, url) + // We don't need to assert the scheme. + assert.True(t, strings.HasSuffix(w.HeaderMap.Get("Location"), expectedLocation), "userAgent: %v, url: %v", tc.userAgent, url) + } + + // This is for unrelated products. + for _, url := range []string{ + "http://test/?product=unknown&os=osx&lang=en-US", + "http://test/?product=notfirefox-nightly-latest-ssl&os=osx&lang=en-US", + "http://test/?product=thunderbird-something-latest-ssl&os=osx&lang=en-US", + "http://test/?product=firefox-115.17.0esr-complete&os=osx&lang=en-US", + "http://test/?product=firefox-115.17.0esr-partial-115.16.1esr&os=osx&lang=en-US", + } { + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", url, nil) + req.Header.Set("User-Agent", tc.userAgent) + + bouncerHandler.ServeHTTP(w, req) + + assert.Equal(t, 404, w.Code, "userAgent: %v, url: %v", tc.userAgent, url) + } + } +} + +func TestBouncerHandlerForMacOSOnlyCompatibleWithESR115WithMozorgReferrer(t *testing.T) { + expectedLocation := "http://download.cdn.mozilla.net/pub/firefox/releases/39.0/mac/en-US/Firefox%2039.0.dmg" + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "http://test/?product=firefox-latest&os=osx&lang=en-US", nil) + req.Header.Set("Referer", "https://www.mozilla.org/") + req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:109.0) Gecko/20100101 Firefox/115.0") + + bouncerHandler.ServeHTTP(w, req) + + assert.Equal(t, 302, w.Code) + assert.Equal(t, expectedLocation, w.HeaderMap.Get("Location")) +} diff --git a/main.go b/main.go index 59a781e..d28afab 100644 --- a/main.go +++ b/main.go @@ -65,7 +65,7 @@ func main() { app.RunAndExitOnError() } -func versionHandler(w http.ResponseWriter, req *http.Request) { +func versionHandler(w http.ResponseWriter, _ *http.Request) { versionFile, err := ioutil.ReadFile(versionFilePath) if err != nil { http.Error(w, "Could not read version file.", http.StatusNotFound) @@ -95,7 +95,7 @@ func Main(c *cli.Context) { bouncerHandler := &BouncerHandler{ db: db, CacheTime: time.Duration(c.Int("cache-time")) * time.Second, - PinHttpsHeaderName: c.String("pin-https-header-name"), + PinHTTPSHeaderName: c.String("pin-https-header-name"), PinnedBaseURLHttp: c.String("pinned-baseurl-http"), PinnedBaseURLHttps: c.String("pinned-baseurl-https"), StubRootURL: c.String("stub-root-url"),