add rewrite functions

This commit is contained in:
hanyixuanten
2026-08-11 21:31:25 +08:00
parent a925475c21
commit 17f561b18b
+258 -4
View File
@@ -59,7 +59,7 @@ foreach ( $_SERVER as $key => $value ) {
if ( substr( $key, 0, 5 ) == 'HTTP_' ) { if ( substr( $key, 0, 5 ) == 'HTTP_' ) {
$headername = str_replace( '_', ' ', substr( $key, 5 ) ); $headername = str_replace( '_', ' ', substr( $key, 5 ) );
$headername = str_replace( ' ', '-', ucwords( strtolower( $headername ) ) ); $headername = str_replace( ' ', '-', ucwords( strtolower( $headername ) ) );
if ( !in_array( $headername, array( 'Host', 'X-Proxy-Url' ) ) ) { if ( !in_array( $headername, array( 'Accept-Encoding', 'Host', 'X-Proxy-Url' ) ) ) {
$request_headers[] = "$headername: $value"; $request_headers[] = "$headername: $value";
} }
} }
@@ -179,15 +179,29 @@ curl_close( $ch );
// split response to header and content // split response to header and content
list($response_headers, $response_content) = preg_split( '/(\r\n){2}/', $response, 2 ); list($response_headers, $response_content) = preg_split( '/(\r\n){2}/', $response, 2 );
// Rewrite links in HTML so subsequent browser requests also pass through this proxy.
$is_html_response = preg_match( '/^Content-Type:\s*(?:text\/html|application\/xhtml\+xml)\b/im', $response_headers );
$is_css_response = preg_match( '/^Content-Type:\s*text\/css\b/im', $response_headers );
if ( $is_html_response ) {
$response_content = csajax_rewrite_html_links( $response_content, $request_url );
} elseif ( $is_css_response ) {
$response_content = csajax_rewrite_css_urls( $response_content, $request_url );
}
$is_rewritten_response = $is_html_response || $is_css_response;
// (re-)send the headers // (re-)send the headers
$response_headers = preg_split( '/(\r\n){1}/', $response_headers ); $response_headers = preg_split( '/(\r\n){1}/', $response_headers );
foreach ( $response_headers as $key => $response_header ) { foreach ( $response_headers as $key => $response_header ) {
// Rewrite the `Location` header, so clients will also use the proxy for redirects. // Rewrite the `Location` header, so clients will also use the proxy for redirects.
if ( preg_match( '/^Location:/', $response_header ) ) { if ( preg_match( '/^Location:/i', $response_header ) ) {
list($header, $value) = preg_split( '/: /', $response_header, 2 ); list($header, $value) = preg_split( '/: /', $response_header, 2 );
$response_header = 'Location: ' . $_SERVER['REQUEST_URI'] . '?csurl=' . $value; $redirect_url = csajax_resolve_url( $request_url, $value );
$response_header = 'Location: ' . csajax_proxy_url( $redirect_url );
} }
if ( !preg_match( '/^(Transfer-Encoding):/', $response_header ) ) { if ( $is_html_response && preg_match( '/^Content-Security-Policy(?:-Report-Only)?:/i', $response_header ) ) {
$response_header = csajax_rewrite_content_security_policy( $response_header );
}
if ( !preg_match( '/^(Transfer-Encoding):/i', $response_header ) && !( $is_rewritten_response && preg_match( '/^Content-Length:/i', $response_header ) ) ) {
header( $response_header, false ); header( $response_header, false );
} }
} }
@@ -201,3 +215,243 @@ function csajax_debug_message( $message )
print $message . PHP_EOL; print $message . PHP_EOL;
} }
} }
function csajax_rewrite_html_links( $html, $document_url )
{
if ( empty( $html ) ) {
return $html;
}
if ( !class_exists( 'DOMDocument' ) ) {
return csajax_rewrite_html_attributes( $html, $document_url );
}
$previous_errors = libxml_use_internal_errors( true );
$document = new DOMDocument();
$loaded = $document->loadHTML( '<?xml encoding="UTF-8">' . $html );
libxml_clear_errors();
libxml_use_internal_errors( $previous_errors );
if ( !$loaded ) {
return csajax_rewrite_html_attributes( $html, $document_url );
}
foreach ( iterator_to_array( $document->childNodes ) as $child ) {
if ( $child->nodeType == XML_PI_NODE || ( $child->nodeType == XML_COMMENT_NODE && strpos( $child->nodeValue, '?xml encoding=' ) === 0 ) ) {
$document->removeChild( $child );
}
}
$base_url = $document_url;
$base_elements = $document->getElementsByTagName( 'base' );
if ( $base_elements->length > 0 && $base_elements->item( 0 )->hasAttribute( 'href' ) ) {
$resolved_base_url = csajax_resolve_url( $document_url, $base_elements->item( 0 )->getAttribute( 'href' ) );
if ( $resolved_base_url !== null ) {
$base_url = $resolved_base_url;
}
$base_elements->item( 0 )->removeAttribute( 'href' );
}
$link_attributes = array( 'href', 'src', 'action', 'formaction', 'poster' );
foreach ( $document->getElementsByTagName( '*' ) as $element ) {
foreach ( $link_attributes as $attribute ) {
if ( !$element->hasAttribute( $attribute ) ) {
continue;
}
$resolved_url = csajax_resolve_url( $base_url, $element->getAttribute( $attribute ) );
if ( $resolved_url !== null ) {
$element->setAttribute( $attribute, csajax_proxy_url( $resolved_url ) );
}
}
if ( $element->hasAttribute( 'srcset' ) ) {
$element->setAttribute( 'srcset', csajax_rewrite_srcset( $element->getAttribute( 'srcset' ), $base_url ) );
}
if ( $element->hasAttribute( 'style' ) ) {
$element->setAttribute( 'style', csajax_rewrite_css_urls( $element->getAttribute( 'style' ), $base_url ) );
}
}
foreach ( $document->getElementsByTagName( 'style' ) as $style_element ) {
$style_element->nodeValue = csajax_rewrite_css_urls( $style_element->nodeValue, $base_url );
}
return $document->saveHTML();
}
function csajax_rewrite_html_attributes( $html, $document_url )
{
$html = preg_replace_callback(
'/\b(href|src|action|formaction|poster|srcset|style)\s*=\s*(?:(["\'])(.*?)\2|([^\s>]+))/is',
function ( $matches ) use ( $document_url ) {
$attribute = strtolower( $matches[1] );
$quote = isset( $matches[2] ) ? $matches[2] : '';
$value = $quote !== '' ? $matches[3] : $matches[4];
if ( $attribute == 'srcset' ) {
$value = csajax_rewrite_srcset( $value, $document_url );
} elseif ( $attribute == 'style' ) {
$value = csajax_rewrite_css_urls( $value, $document_url );
} else {
$resolved_url = csajax_resolve_url( $document_url, $value );
if ( $resolved_url !== null ) {
$value = csajax_proxy_url( $resolved_url );
}
}
return $matches[1] . '=' . $quote . $value . $quote;
},
$html
);
return preg_replace_callback(
'/<style\b([^>]*)>(.*?)<\/style>/is',
function ( $matches ) use ( $document_url ) {
return '<style' . $matches[1] . '>' . csajax_rewrite_css_urls( $matches[2], $document_url ) . '</style>';
},
$html
);
}
function csajax_rewrite_css_urls( $css, $document_url )
{
$css = preg_replace_callback(
'/url\(\s*(["\']?)(.*?)\1\s*\)/i',
function ( $matches ) use ( $document_url ) {
$resolved_url = csajax_resolve_url( $document_url, $matches[2] );
if ( $resolved_url === null ) {
return $matches[0];
}
return 'url(' . $matches[1] . csajax_proxy_url( $resolved_url ) . $matches[1] . ')';
},
$css
);
return preg_replace_callback(
'/@import\s+(["\'])(.*?)\1/i',
function ( $matches ) use ( $document_url ) {
$resolved_url = csajax_resolve_url( $document_url, $matches[2] );
if ( $resolved_url === null ) {
return $matches[0];
}
return '@import ' . $matches[1] . csajax_proxy_url( $resolved_url ) . $matches[1];
},
$css
);
}
function csajax_rewrite_srcset( $srcset, $base_url )
{
$candidates = array();
foreach ( explode( ',', $srcset ) as $candidate ) {
$candidate = trim( $candidate );
if ( $candidate === '' ) {
continue;
}
$parts = preg_split( '/\s+/', $candidate, 2 );
$resolved_url = csajax_resolve_url( $base_url, $parts[0] );
if ( $resolved_url !== null ) {
$parts[0] = csajax_proxy_url( $resolved_url );
}
$candidates[] = implode( ' ', $parts );
}
return implode( ', ', $candidates );
}
function csajax_rewrite_content_security_policy( $header )
{
list($header_name, $policy) = preg_split( '/:\s*/', $header, 2 );
$proxy_directives = array(
'default-src', 'script-src', 'script-src-elem', 'style-src', 'style-src-elem', 'img-src',
'media-src', 'frame-src', 'child-src', 'manifest-src', 'form-action'
);
$directives = array();
foreach ( explode( ';', $policy ) as $directive ) {
$directive = trim( $directive );
if ( $directive === '' ) {
continue;
}
$parts = preg_split( '/\s+/', $directive );
$name = strtolower( $parts[0] );
if ( in_array( $name, $proxy_directives ) && !in_array( "'self'", $parts ) ) {
$parts[] = "'self'";
}
$directives[] = implode( ' ', $parts );
}
return $header_name . ': ' . implode( '; ', $directives );
}
function csajax_proxy_url( $target_url )
{
return $_SERVER['SCRIPT_NAME'] . '?csurl=' . rawurlencode( $target_url );
}
function csajax_resolve_url( $base_url, $url )
{
$url = trim( html_entity_decode( $url, ENT_QUOTES | ENT_HTML5, 'UTF-8' ) );
if ( $url === '' || $url[0] == '#' || preg_match( '/^(?:data|javascript|mailto|tel|blob|about):/i', $url ) ) {
return null;
}
if ( preg_match( '!^https?://!i', $url ) ) {
return $url;
}
$base = parse_url( $base_url );
if ( $base === false || !isset( $base['scheme'], $base['host'] ) ) {
return null;
}
$origin = $base['scheme'] . '://' . $base['host'];
if ( isset( $base['port'] ) ) {
$origin .= ':' . $base['port'];
}
if ( substr( $url, 0, 2 ) == '//' ) {
return $base['scheme'] . ':' . $url;
}
$relative = parse_url( $url );
if ( $relative === false ) {
return null;
}
if ( isset( $relative['path'] ) && substr( $relative['path'], 0, 1 ) == '/' ) {
$path = $relative['path'];
} elseif ( isset( $relative['path'] ) && $relative['path'] !== '' ) {
$base_path = isset( $base['path'] ) ? $base['path'] : '/';
$path = preg_replace( '!/[^/]*$!', '/', $base_path ) . $relative['path'];
} else {
$path = isset( $base['path'] ) ? $base['path'] : '/';
}
$segments = array();
foreach ( explode( '/', $path ) as $segment ) {
if ( $segment === '' || $segment === '.' ) {
continue;
}
if ( $segment === '..' ) {
array_pop( $segments );
} else {
$segments[] = $segment;
}
}
$resolved_url = $origin . '/' . implode( '/', $segments );
if ( substr( $path, -1 ) == '/' && substr( $resolved_url, -1 ) != '/' ) {
$resolved_url .= '/';
}
if ( array_key_exists( 'query', $relative ) ) {
$resolved_url .= '?' . $relative['query'];
} elseif ( !isset( $relative['path'] ) && isset( $base['query'] ) ) {
$resolved_url .= '?' . $base['query'];
}
if ( isset( $relative['fragment'] ) ) {
$resolved_url .= '#' . $relative['fragment'];
}
return $resolved_url;
}