|
613 | 613 | return urlObj.toString(); |
614 | 614 | } |
615 | 615 |
|
| 616 | + // GitHub blob viewer: convert to raw content URL |
| 617 | + // Format: https://github.com/{user}/{repo}/blob/{ref}/{path} |
| 618 | + // Convert to: https://raw.githubusercontent.com/{user}/{repo}/{ref}/{path} |
| 619 | + if (urlObj.hostname === 'github.com') { |
| 620 | + const match = urlObj.pathname.match(/^\/([^/]+)\/([^/]+)\/blob\/(.+)$/); |
| 621 | + if (match) { |
| 622 | + return `https://raw.githubusercontent.com/${match[1]}/${match[2]}/${match[3]}`; |
| 623 | + } |
| 624 | + } |
| 625 | + |
616 | 626 | // Return original URL if no conversion needed |
617 | 627 | return url; |
618 | 628 | } catch (e) { |
|
696 | 706 | * Each proxy has a different URL format |
697 | 707 | */ |
698 | 708 | const CORS_PROXIES = [ |
| 709 | + { url: 'https://github-proxy.exelearning.dev/?url=', encode: true, githubOnly: true }, |
699 | 710 | { url: 'https://corsproxy.io/?', encode: true }, |
700 | 711 | { url: 'https://api.allorigins.win/raw?url=', encode: true }, |
701 | 712 | { url: 'https://cors.eu.org/', encode: false } |
702 | 713 | ]; |
703 | 714 |
|
| 715 | + const GITHUB_HOSTNAMES = new Set([ |
| 716 | + 'github.com', |
| 717 | + 'raw.githubusercontent.com', |
| 718 | + 'gist.githubusercontent.com', |
| 719 | + 'objects.githubusercontent.com', |
| 720 | + 'codeload.github.com', |
| 721 | + 'releases.githubusercontent.com', |
| 722 | + 'media.githubusercontent.com' |
| 723 | + ]); |
| 724 | + |
| 725 | + function isGithubUrl(url) { |
| 726 | + try { |
| 727 | + return GITHUB_HOSTNAMES.has(new URL(url).hostname); |
| 728 | + } catch { |
| 729 | + return false; |
| 730 | + } |
| 731 | + } |
| 732 | + |
704 | 733 | /** |
705 | 734 | * Fetch with CORS proxy fallback |
706 | 735 | * @param {string} url - The URL to fetch |
|
717 | 746 | } |
718 | 747 |
|
719 | 748 | // Try each proxy until one works |
| 749 | + const isGithub = isGithubUrl(url); |
720 | 750 | let lastError; |
721 | 751 | for (const proxy of CORS_PROXIES) { |
722 | | - try { |
723 | | - const proxyUrl = proxy.encode |
724 | | - ? proxy.url + encodeURIComponent(url) |
725 | | - : proxy.url + url; |
| 752 | + if (proxy.githubOnly && !isGithub) continue; |
| 753 | + |
| 754 | + const proxyUrl = proxy.encode |
| 755 | + ? proxy.url + encodeURIComponent(url) |
| 756 | + : proxy.url + url; |
726 | 757 |
|
| 758 | + const controller = new AbortController(); |
| 759 | + const timer = setTimeout(() => controller.abort(), 8000); |
| 760 | + |
| 761 | + try { |
727 | 762 | console.log(`[App] Trying proxy: ${proxy.url}`); |
728 | 763 | const response = await fetch(proxyUrl, { |
729 | 764 | method: 'GET', |
730 | 765 | mode: 'cors', |
731 | | - credentials: 'omit' |
| 766 | + credentials: 'omit', |
| 767 | + signal: controller.signal |
732 | 768 | }); |
| 769 | + clearTimeout(timer); |
733 | 770 |
|
734 | 771 | if (response.ok) { |
735 | 772 | console.log(`[App] Proxy ${proxy.url} succeeded`); |
736 | 773 | return response; |
737 | 774 | } |
738 | 775 | } catch (err) { |
| 776 | + clearTimeout(timer); |
739 | 777 | console.warn(`[App] Proxy ${proxy.url} failed:`, err.message); |
740 | 778 | lastError = err; |
741 | 779 | } |
|
0 commit comments