(client *http.Client, imageURL string, referer string)
| 1780 | } |
| 1781 | |
| 1782 | func inlineSmallImageFromURL(client *http.Client, imageURL string, referer string) string { |
| 1783 | if imageURL == "" { |
| 1784 | return "" |
| 1785 | } |
| 1786 | low := strings.ToLower(imageURL) |
| 1787 | // HTTPS-only: subscription icon URLs come from server-controlled headers |
| 1788 | // or HTML, so a hostile server could embed an http:// URL and observe |
| 1789 | // our request unencrypted. Refusing http here costs nothing — almost |
| 1790 | // every modern site serves icons over HTTPS. |
| 1791 | if !strings.HasPrefix(low, "https://") { |
| 1792 | return "" |
| 1793 | } |
| 1794 | ctx, cancel := context.WithTimeout(context.Background(), 6*time.Second) |
| 1795 | defer cancel() |
| 1796 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, imageURL, nil) |
| 1797 | if err != nil { |
| 1798 | return "" |
| 1799 | } |
| 1800 | req.Header.Set("User-Agent", subscriptionPageUserAgent) |
| 1801 | req.Header.Set("Accept", "image/avif,image/webp,image/apng,image/*,*/*;q=0.8") |
| 1802 | // Referer reveals the subscription URL (often containing a token) to |
| 1803 | // every third-party host that serves an icon. Only attach it when the |
| 1804 | // icon host matches the subscription host — that's the legitimate |
| 1805 | // "icon hosted by the provider" case. |
| 1806 | if rh := sameHostReferer(referer, imageURL); rh != "" { |
| 1807 | req.Header.Set("Referer", rh) |
| 1808 | } |
| 1809 | // Sandbox the connection through our SSRF-aware dialer. The default |
| 1810 | // client passed in shares the cookie jar but we override the transport. |
| 1811 | // DisableKeepAlives + CloseIdleConnections on return guarantees this |
| 1812 | // per-call Transport doesn't park idle sockets in a pool that nothing |
| 1813 | // will ever drain — icon fetches are one-shot and the Transport itself |
| 1814 | // is discarded after this function returns, so any kept-alive socket |
| 1815 | // would just dangle in the OS FD table until GC eventually finalised it. |
| 1816 | transport := &http.Transport{ |
| 1817 | DialContext: safeImageDialer().DialContext, |
| 1818 | TLSHandshakeTimeout: 5 * time.Second, |
| 1819 | ResponseHeaderTimeout: 6 * time.Second, |
| 1820 | DisableKeepAlives: true, |
| 1821 | } |
| 1822 | defer transport.CloseIdleConnections() |
| 1823 | safeClient := *client |
| 1824 | safeClient.Transport = transport |
| 1825 | resp, err := safeClient.Do(req) |
| 1826 | if err != nil || resp.StatusCode != http.StatusOK { |
| 1827 | if resp != nil { |
| 1828 | resp.Body.Close() |
| 1829 | } |
| 1830 | return "" |
| 1831 | } |
| 1832 | defer resp.Body.Close() |
| 1833 | const maxBytes = 262144 |
| 1834 | buf, err := io.ReadAll(io.LimitReader(resp.Body, maxBytes+1)) |
| 1835 | if err != nil || len(buf) > maxBytes { |
| 1836 | return "" |
| 1837 | } |
| 1838 | ct := imageContentTypeFromBytes(buf, resp.Header.Get("Content-Type")) |
| 1839 | if ct == "" { |
no test coverage detected