前言

免费使用 GitHub 私有仓库部署网站一文介绍了如何部署github pages,并实现了自定义域名的配置。本文介绍另一种自定义域名来访问github pages的方法, 特点是:

  • 访客无法通过user.me 追踪到真实的user.github.io
  • 通过cloudflare worker来实现 user.me 指向 user.github.io

整个访问流程如下:

用户访问 user.me
↓
Cloudflare DNS (指向 Worker)
↓
Worker 处理请求 (隐藏源站)
↓
Worker 访问 user.github.io
↓
GitHub Pages 返回内容

这种方案的优势:

  1. 安全性更高:完全隐藏GitHub Pages源站
  2. 配置简单:无需复杂的DNS设置
  3. 更少出错:避免DNS解析冲突
  4. 维护方便:集中化管理

注意:使用这种方法时,GitHub Pages只需保持默认的xxx.github.io域名即可,无需配置CNAME文件。

具体实现

1. 创建cf worker

首先创建一个名为hide-ghpage的Worker, 在设置中添加环境变量:

  • GITHUB_PAGES 填写 xxx.github.io
  • CUSTOM_DOMAIN 填写 user.me
  • WWW_DOMAIN 填写 www.user.me
  • WORKER_DOMAIN 填写 hide-ghpage.user.workers.dev

然后编辑代码如下:

function encodePathProperly(path) {
    // 先解码以防重复编码
    const decodedPath = decodeURIComponent(path);
    
    // 替换特殊字符,之前被这个坑惨了。由于网页中包含邮件地址,特殊字符(如 @)在 URL 中无法正确处理,一直显示404.
    return decodedPath
        .split('/')
        .map(segment => 
            segment
                .replace(/@/g, '%40')  // 替换@
                .replace(/#/g, '%23')
                .replace(/\?/g, '%3F')
                .replace(/\[/g, '%5B')
                .replace(/\]/g, '%5D')
                .replace(/=/g, '%3D')
                .replace(/&/g, '%26')
        )
        .join('/');
 }
 
 function initializeConfig(env) {
    console.log('Initializing with env vars:', {
        GITHUB_PAGES: env.GITHUB_PAGES,
        CUSTOM_DOMAIN: env.CUSTOM_DOMAIN,
        WWW_DOMAIN: env.WWW_DOMAIN,
        WORKER_DOMAIN: env.WORKER_DOMAIN
    });
 
    const DOMAINS = {
        GITHUB: env.GITHUB_PAGES,
        CUSTOM: env.CUSTOM_DOMAIN,
        WWW: env.WWW_DOMAIN,
        WORKER: env.WORKER_DOMAIN
    };
 
    return {
        domains: DOMAINS,
        githubPages: DOMAINS.GITHUB,
        allowedDomains: [DOMAINS.CUSTOM, DOMAINS.WWW, DOMAINS.WORKER],
        sensitiveHeaders: [
            'server',
            'x-powered-by',
            'x-github-request-id',
            'x-fastly',
            'x-proxy-cache',
            'via',
            'x-served-by',
            'x-cache',
            'x-cache-hits',
            'x-timer',
            'x-fastly-request-id',
            'x-request-id',
            'x-runtime',
            'x-robots-tag',
            'alt-svc',
            'x-github',
            'x-origin-cache',
            'x-content-type-options'
        ],
        customHeaders: {
            'server': 'cloudflare',
            'X-Frame-Options': 'SAMEORIGIN',
            'X-Content-Type-Options': 'nosniff',
            'Referrer-Policy': 'strict-origin-when-cross-origin',
            'Permissions-Policy': 'interest-cohort=()',
            'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';",
            'X-Powered-By': 'Cloudflare'
        },
        debug: true
    };
 }
 
 async function logError(error, request, extra = {}) {
    console.error('Error:', {
        message: error.message,
        stack: error.stack,
        url: request.url,
        method: request.method,
        ...extra
    });
 }
 
 async function handleRequest(request, env) {
    const config = initializeConfig({
        GITHUB_PAGES: env.GITHUB_PAGES,
        CUSTOM_DOMAIN: env.CUSTOM_DOMAIN,
        WWW_DOMAIN: env.WWW_DOMAIN,
        WORKER_DOMAIN: env.WORKER_DOMAIN
    });
    const { domains } = config;
    
    try {
        const url = new URL(request.url);
        const currentDomain = url.hostname;
 
        // URL 编码处理
        const encodedPath = encodePathProperly(url.pathname);
        console.log('URL encoding:', {
            originalPath: url.pathname,
            encodedPath: encodedPath
        });
 
        // 详细的请求日志
        console.log('Request details:', {
            url: url.toString(),
            domain: currentDomain,
            path: encodedPath,
            allowedDomains: config.allowedDomains,
            method: request.method,
            headers: Object.fromEntries(request.headers)
        });
 
        // www 重定向到主域名
        if (currentDomain === domains.WWW) {
            const newUrl = `https://${domains.CUSTOM}${encodedPath}${url.search}`;
            console.log('Redirecting www to non-www:', {
                from: url.toString(),
                to: newUrl
            });
            
            return new Response(null, {
                status: 301,
                headers: {
                    'Location': newUrl,
                    'Cache-Control': 'no-cache, no-store, must-revalidate',
                    'Pragma': 'no-cache',
                    'Expires': '0',
                    'server': 'cloudflare'
                }
            });
        }
 
        // 验证域名
        if (!config.allowedDomains.includes(currentDomain)) {
            console.warn('Invalid domain access:', {
                domain: currentDomain,
                allowedDomains: config.allowedDomains
            });
            return new Response('Domain not allowed', { status: 403 });
        }
 
        // 构建 GitHub URL
        const githubUrl = `https://${config.githubPages}${encodedPath}${url.search}`;
        
        console.log('Proxying to GitHub:', {
            from: url.toString(),
            to: githubUrl
        });
 
        // 获取文件扩展名
        const ext = encodedPath.split('.').pop().toLowerCase();
        const isAsset = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'css', 'js', 'svg', 'ico', 'woff', 'woff2', 'ttf'].includes(ext);
 
        // 处理请求头
        let newHeaders = new Headers(request.headers);
        
        // 清理敏感请求头
        ['referer', 'origin', 'x-real-ip', 'cf-connecting-ip', 'x-forwarded-for', 'x-forwarded-proto', 'cookie'].forEach(header => {
            newHeaders.delete(header);
        });
 
        // 设置新的请求头
        newHeaders.set('Host', config.githubPages);
        newHeaders.set('Accept-Encoding', 'gzip, deflate, br');
        newHeaders.set('User-Agent', request.headers.get('User-Agent') || 'Cloudflare Worker');
 
        // 创建 GitHub 请求
        const githubRequest = new Request(githubUrl, {
            method: request.method,
            headers: newHeaders,
            redirect: isAsset ? 'follow' : 'manual',
            cf: {
                cacheEverything: false,
                cacheTtl: isAsset ? 86400 : 0,
                timeout: 30000
            }
        });
 
        // 发送请求到 GitHub Pages
        let response = await fetch(githubRequest);
        
        console.log('GitHub response:', {
            status: response.status,
            statusText: response.statusText,
            headers: Object.fromEntries(response.headers),
            url: githubUrl
        });
 
        // 如果是 404,增加调试日志
        if (response.status === 404) {
            console.log('404 Error details:', {
                githubUrl,
                originalUrl: request.url,
                headers: Object.fromEntries(response.headers),
                domain: currentDomain,
                githubPages: config.githubPages
            });
 
            // 尝试直接访问 GitHub Pages
            try {
                const testResponse = await fetch(`https://${config.githubPages}${encodedPath}`);
                console.log('Direct GitHub test:', {
                    status: testResponse.status,
                    headers: Object.fromEntries(testResponse.headers)
                });
            } catch (error) {
                console.error('Direct GitHub test failed:', error);
            }
        }
 
        // 处理资源文件
        if (isAsset) {
            let newResponse = new Response(response.body, {
                status: response.status,
                statusText: response.statusText,
                headers: new Headers(response.headers)
            });
 
            const allowedHeaders = [
                'content-type',
                'content-length',
                'cache-control',
                'content-encoding',
                'etag',
                'last-modified'
            ];
 
            for (const key of [...newResponse.headers.keys()]) {
                if (!allowedHeaders.includes(key.toLowerCase())) {
                    newResponse.headers.delete(key);
                }
            }
 
            if (response.ok) {
                newResponse.headers.set('Cache-Control', 'public, max-age=31536000');
            }
            return newResponse;
        }
 
        // 处理重定向
        if ([301, 302, 307, 308].includes(response.status)) {
            const location = response.headers.get('Location');
            
            console.log('Handling redirect:', {
                from: url.toString(),
                location: location
            });
 
            if (location) {
                const redirectUrl = new URL(location.startsWith('http') ? location : `https://${config.githubPages}${location}`);
                const newLocation = `https://${currentDomain}${redirectUrl.pathname}${redirectUrl.search}`;
                
                console.log('New redirect location:', newLocation);
 
                return new Response(null, {
                    status: response.status,
                    headers: {
                        'Location': newLocation,
                        'Cache-Control': 'no-cache, no-store, must-revalidate',
                        'Pragma': 'no-cache',
                        'Expires': '0',
                        'server': 'cloudflare'
                    }
                });
            }
        }
 
        // 读取响应体
        let body = await response.text();
 
        // 替换规则
        const replacements = [
            [new RegExp(`https?://${config.githubPages}`, 'g'), `https://${currentDomain}`],
            [`${config.githubPages}`, currentDomain],
            [/href="\//g, `href="https://${currentDomain}/`],
            [/src="\//g, `src="https://${currentDomain}/`],
            [/src="([^"]+)"/g, (match, p1) => {
                if (p1.startsWith('http')) {
                    return match.replace(config.githubPages, currentDomain);
                } else if (p1.startsWith('/')) {
                    return `src="https://${currentDomain}${p1}"`;
                }
                return match;
            }],
            [/srcset="([^"]+)"/g, (match, p1) => {
                return `srcset="${p1.split(',').map(src => {
                    const [url, size] = src.trim().split(' ');
                    if (url.startsWith('http')) {
                        return `${url.replace(config.githubPages, currentDomain)} ${size || ''}`;
                    } else if (url.startsWith('/')) {
                        return `https://${currentDomain}${url} ${size || ''}`;
                    }
                    return `${url} ${size || ''}`;
                }).join(', ')}"`;
            }],
            [/<meta[^>]*?content="[^"]*?github\.io[^"]*?"[^>]*?>/g, (match) => {
                return match.replace(config.githubPages, currentDomain);
            }],
            [/<link[^>]*?rel="canonical"[^>]*?href="[^"]*?"[^>]*?>/g, (match) => {
                return match.replace(config.githubPages, currentDomain);
            }],
            [/<script type="application\/ld\+json">[^<]*?<\/script>/g, (match) => {
                return match.replace(config.githubPages, currentDomain);
            }]
        ];
 
        // 应用替换规则
        replacements.forEach(([pattern, replacement]) => {
            body = body.replace(pattern, replacement);
        });
 
        // 创建最终响应
        let newResponse = new Response(body, {
            status: response.status,
            statusText: response.statusText,
            headers: new Headers({
                'Content-Type': response.headers.get('Content-Type') || 'text/html;charset=UTF-8',
                'Cache-Control': 'no-cache, no-store, must-revalidate',
                'Pragma': 'no-cache',
                'Expires': '0'
            })
        });
 
        // 添加安全头部
        config.sensitiveHeaders.forEach(header => {
            newResponse.headers.delete(header);
        });
 
        Object.entries(config.customHeaders).forEach(([key, value]) => {
            newResponse.headers.set(key, value);
        });
 
        return newResponse;
 
    } catch (err) {
        await logError(err, request);
        return new Response(`Error: ${err.message}`, { 
            status: 500,
            headers: {
                'Content-Type': 'text/plain',
                'Cache-Control': 'no-cache, no-store, must-revalidate',
                'Pragma': 'no-cache',
                'Expires': '0',
                'server': 'cloudflare'
            }
        });
    }
 }
 
 export default {
    async fetch(request, env, ctx) {
        // 记录环境变量状态
        console.log('Environment variables:', {
            GITHUB_PAGES: env.GITHUB_PAGES,
            CUSTOM_DOMAIN: env.CUSTOM_DOMAIN,
            WWW_DOMAIN: env.WWW_DOMAIN,
            WORKER_DOMAIN: env.WORKER_DOMAIN
        });
 
        try {
            // 记录请求信息
            console.log('Incoming request:', {
                url: request.url,
                method: request.method,
                headers: Object.fromEntries(request.headers)
            });
 
            const response = await handleRequest(request, env);
 
            // 记录响应信息
            console.log('Response details:', {
                status: response.status,
                headers: Object.fromEntries(response.headers)
            });
 
            return response;
        } catch (error) {
            console.error('Fatal error:', error);
            return new Response('Internal Server Error', { status: 500 });
        }
    }
 };

2. 配置DNS解析

要将自定义域名指向 Worker,需要完成以下配置:

  1. 进入 Cloudflare Worker 设置页面
  2. 在"自定义域名"(Custom Domains)选项下
  3. 依次添加域名 user.mewww.user.me(Cloudflare 将自动创建对应的 DNS 记录)
  4. 在"路由"(Zone Routes)中添加以下区域: user.me, 路由:user.me/*www.user.me/*

最后结果如下:

workers.dev 子域名:hide-ghpage.user.workers.dev
自定义域:user.me
自定义域:www.user.me
路由:user.me/*
路由:www.user.me/*

自定义域名与区域路由的区别

  • 自定义域名:声明某个域名可以使用该 Worker 服务
  • 区域路由:定义 Worker 具体要处理的 URL 访问规则

举例说明:如果只配置了自定义域名而没有添加路由规则,Worker 虽然知道它可以为 user.me 提供服务,但不清楚具体要处理哪些 URL 请求。

重要提醒 完成以上配置后,请务必:

  • 删除所有指向 GitHub IP 的 A 记录
  • 删除所有指向 user.github.io 的 CNAME 记录
  • 仅保留指向 Worker 的 DNS 记录

这样可以确保所有流量都经过 Worker 处理,从而实现完整的域名代理功能。

3. 配置SSL/TLS

为确保安全访问,需要正确配置SSL:

  1. SSL/TLS > 概述

    • 选择"完全"(Full)模式
  2. SSL/TLS > 边缘证书

    • ✅ 启用"始终使用HTTPS"
    • ✅ 启用"自动HTTPS重写"

这样可以确保:

  • HTTP自动跳转HTTPS
  • 所有子域名启用HTTPS
  • 自动修复混合内容问题

4. 配置缓存规则

为提升访问速度,建议配置以下缓存规则:

  1. 创建新规则
规则名称:静态资源缓存
匹配条件:所有传入请求
  1. 设置缓存条件
字段:URI 完整
运算符:通配符
值:https://user.me/*.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf)
  1. 配置缓存参数
缓存资格:符合缓存条件
边缘TTL:4小时
浏览器TTL:4小时

缓存配置说明:

  • 静态资源默认缓存4小时
  • 同时在CDN节点和浏览器端缓存
  • 可根据需求调整缓存时间
    • 频繁更新的资源建议缩短时间
    • 稳定资源可适当延长

通过以上配置,我们成功实现了:

  • 隐藏GitHub Pages源站
  • 提升访问速度
  • 增强安全性
  • 优化用户体验

注意事项

如果完成上述配置还无法正常访问,可以尝试:

  • 尝试先通过https://hide-ghpage.user.workers.dev访问
  • 尝试在浏览器的无痕模式访问user.me
  • 在cf上清除user.mewww.user.me 的dns 缓存
  • 本地清除dns缓存
  • 删除浏览器的浏览记录和cookies
  • 如果设置了页面规则"从 WWW 重定向到根",请删除

总结

通过上述设置,应该可以实现Cloudflare Workers 代理 GitHub Pages,并隐藏原始的域名地址。