Skip to content

Commit b74db55

Browse files
authored
Merge pull request #1 from RebelMouseTeam/feat/proxy-mode
Add proxy mode for WordPress plugin
2 parents 3ffefca + 35298d3 commit b74db55

6 files changed

Lines changed: 259 additions & 3 deletions

File tree

assets/js/admin.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
nonce: rebelboost.nonce,
1616
api_key: $('#rebelboost_api_key').val(),
1717
host: $('#rebelboost_host').val() || '',
18+
mode: $('input[name="rebelboost_mode"]:checked').val() || 'integration',
1819
})
1920
.done(function (response) {
2021
if (response.success) {

includes/class-api-client.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,14 @@ public function reload() {
2525
*
2626
* @return true|WP_Error
2727
*/
28-
public function test_connection() {
28+
/**
29+
* Test the connection by attempting a purge on a non-existent path.
30+
*
31+
* @param bool $proxy_mode When true, accept 400 as valid (host may not
32+
* have a purge config yet, but the key was not rejected).
33+
* @return true|WP_Error
34+
*/
35+
public function test_connection( $proxy_mode = false ) {
2936
if ( empty( $this->api_key ) ) {
3037
return new WP_Error( 'rebelboost_not_configured', __( 'API key is required.', 'rebelboost' ) );
3138
}
@@ -46,6 +53,12 @@ public function test_connection() {
4653
return true;
4754
}
4855

56+
// In proxy mode the host may not have a purge config yet,
57+
// so 400 is expected. The key was accepted (not 401/403).
58+
if ( $proxy_mode && 400 === $code ) {
59+
return true;
60+
}
61+
4962
return new WP_Error(
5063
'rebelboost_unexpected_response',
5164
sprintf(

includes/class-cli.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ public function status() {
9595
return;
9696
}
9797

98+
$mode = get_option( 'rebelboost_mode', 'integration' );
99+
100+
WP_CLI::log( "Mode: {$mode}" );
98101
WP_CLI::log( "Host: {$host}" );
99102
WP_CLI::log( 'API Key: ' . substr( $api_key, 0, 8 ) . str_repeat( '*', max( 0, strlen( $api_key ) - 8 ) ) );
100103

@@ -107,6 +110,38 @@ public function status() {
107110
WP_CLI::success( 'RebelBoost is configured.' );
108111
}
109112

113+
/**
114+
* Show or set the plugin operating mode.
115+
*
116+
* ## OPTIONS
117+
*
118+
* [<mode>]
119+
* : Set mode to 'integration' or 'proxy'. Omit to show current mode.
120+
*
121+
* ## EXAMPLES
122+
*
123+
* wp rebelboost mode
124+
* wp rebelboost mode proxy
125+
* wp rebelboost mode integration
126+
*
127+
* @subcommand mode
128+
*/
129+
public function mode( $args ) {
130+
if ( empty( $args[0] ) ) {
131+
$current = get_option( 'rebelboost_mode', 'integration' );
132+
WP_CLI::log( "Current mode: {$current}" );
133+
return;
134+
}
135+
136+
$new_mode = $args[0];
137+
if ( ! in_array( $new_mode, array( 'integration', 'proxy' ), true ) ) {
138+
WP_CLI::error( "Invalid mode: {$new_mode}. Use 'integration' or 'proxy'." );
139+
}
140+
141+
update_option( 'rebelboost_mode', $new_mode );
142+
WP_CLI::success( "Mode set to: {$new_mode}" );
143+
}
144+
110145
/**
111146
* Test the connection to RebelBoost.
112147
*

includes/class-proxy-mode.php

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
<?php
2+
3+
defined( 'ABSPATH' ) || exit;
4+
5+
/**
6+
* Proxy mode — serves optimized pages via the RebelBoost service.
7+
*
8+
* For regular browser requests, this class fetches the page from the
9+
* RebelBoost proxy (which applies optimizations like image compression,
10+
* lazy loading, script deferral, etc.) and serves the optimized response.
11+
*
12+
* When the RebelBoost service fetches back from WordPress (origin fetch),
13+
* we detect this via a secret header token and let WordPress serve normally
14+
* to avoid an infinite loop.
15+
*/
16+
class RebelBoost_Proxy_Mode {
17+
18+
private $site_host;
19+
private $base_url;
20+
21+
/**
22+
* Secret token used to identify origin fetches from the plugin's own
23+
* proxy requests vs. the RebelBoost service fetching from origin.
24+
* Derived from the API key so it's unique per site but not guessable.
25+
*/
26+
private $loop_token;
27+
28+
public function __construct() {
29+
$this->site_host = wp_parse_url( site_url(), PHP_URL_HOST );
30+
$override = get_option( 'rebelboost_host', '' );
31+
$this->base_url = ! empty( $override ) ? untrailingslashit( $override ) : 'https://ingressv2.rebelboost.com';
32+
$this->loop_token = substr( md5( 'rebelboost_proxy_' . get_option( 'rebelboost_api_key', '' ) ), 0, 16 );
33+
}
34+
35+
public function register_hooks() {
36+
if ( ! $this->is_active() ) {
37+
return;
38+
}
39+
40+
// If this request was made by our own proxy (loopback from RebelBoost
41+
// service fetching the origin), let WordPress serve normally.
42+
if ( $this->is_loopback() ) {
43+
return;
44+
}
45+
46+
// For regular browser requests, proxy through RebelBoost.
47+
add_action( 'template_redirect', array( $this, 'serve_optimized' ), 0 );
48+
}
49+
50+
public function is_active() {
51+
return 'proxy' === get_option( 'rebelboost_mode', 'integration' )
52+
&& RebelBoost::is_connected()
53+
&& ! is_admin()
54+
&& ! wp_doing_ajax()
55+
&& ! wp_doing_cron()
56+
&& ! ( defined( 'REST_REQUEST' ) && REST_REQUEST );
57+
}
58+
59+
/**
60+
* Detect if this is an origin fetch triggered by our own proxy request.
61+
*
62+
* The RebelBoost service forwards all request headers to the origin.
63+
* We add a custom header (X-Rebelboost-Loop-Token) when proxying,
64+
* which gets forwarded back to us on the origin fetch.
65+
*/
66+
private function is_loopback() {
67+
$token = isset( $_SERVER['HTTP_X_REBELBOOST_LOOP_TOKEN'] )
68+
? $_SERVER['HTTP_X_REBELBOOST_LOOP_TOKEN']
69+
: '';
70+
return $token === $this->loop_token;
71+
}
72+
73+
/**
74+
* Fetch the optimized page from the RebelBoost service and serve it.
75+
*/
76+
public function serve_optimized() {
77+
$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : '/';
78+
$url = $this->base_url . $request_uri;
79+
80+
$headers = array(
81+
'Host' => $this->site_host,
82+
'X-Forwarded-Host' => $this->site_host,
83+
'X-Forwarded-For' => isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : '',
84+
'X-Rebelboost-Loop-Token' => $this->loop_token,
85+
'Accept' => isset( $_SERVER['HTTP_ACCEPT'] ) ? $_SERVER['HTTP_ACCEPT'] : '*/*',
86+
'Accept-Encoding' => 'identity',
87+
'User-Agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : '',
88+
);
89+
90+
// Forward cookies for authenticated/personalized content.
91+
if ( ! empty( $_SERVER['HTTP_COOKIE'] ) ) {
92+
$headers['Cookie'] = $_SERVER['HTTP_COOKIE'];
93+
}
94+
95+
$response = wp_remote_get( $url, array(
96+
'headers' => $headers,
97+
'timeout' => 15,
98+
'redirection' => 0,
99+
'decompress' => true,
100+
) );
101+
102+
if ( is_wp_error( $response ) ) {
103+
// If the proxy is unreachable, fall back to normal WordPress output.
104+
return;
105+
}
106+
107+
$status = wp_remote_retrieve_response_code( $response );
108+
$body = wp_remote_retrieve_body( $response );
109+
110+
// Forward relevant response headers.
111+
$passthrough_headers = array(
112+
'content-type',
113+
'cache-control',
114+
'x-rebelboost-cache',
115+
'x-rebelboost-optimized',
116+
'vary',
117+
'link',
118+
);
119+
120+
foreach ( $passthrough_headers as $name ) {
121+
$value = wp_remote_retrieve_header( $response, $name );
122+
if ( ! empty( $value ) ) {
123+
header( $name . ': ' . $value );
124+
}
125+
}
126+
127+
http_response_code( $status );
128+
echo $body; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
129+
exit;
130+
}
131+
}

includes/class-rebelboost.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class RebelBoost {
1111
private $surrogate_keys;
1212
private $settings;
1313
private $admin;
14+
private $proxy_mode;
1415

1516
public static function get_instance() {
1617
if ( null === self::$instance ) {
@@ -31,6 +32,7 @@ private function load_dependencies() {
3132
require_once REBELBOOST_PLUGIN_DIR . 'includes/class-surrogate-keys.php';
3233
require_once REBELBOOST_PLUGIN_DIR . 'includes/class-settings.php';
3334
require_once REBELBOOST_PLUGIN_DIR . 'includes/class-admin.php';
35+
require_once REBELBOOST_PLUGIN_DIR . 'includes/class-proxy-mode.php';
3436

3537
if ( defined( 'WP_CLI' ) && WP_CLI ) {
3638
require_once REBELBOOST_PLUGIN_DIR . 'includes/class-cli.php';
@@ -41,13 +43,15 @@ private function load_dependencies() {
4143
$this->surrogate_keys = new RebelBoost_Surrogate_Keys();
4244
$this->settings = new RebelBoost_Settings( $this->api_client );
4345
$this->admin = new RebelBoost_Admin( $this->api_client );
46+
$this->proxy_mode = new RebelBoost_Proxy_Mode();
4447
}
4548

4649
private function register_hooks() {
4750
$this->cache_invalidation->register_hooks();
4851
$this->surrogate_keys->register_hooks();
4952
$this->settings->register_hooks();
5053
$this->admin->register_hooks();
54+
$this->proxy_mode->register_hooks();
5155

5256
if ( defined( 'WP_CLI' ) && WP_CLI ) {
5357
RebelBoost_CLI::register( $this->api_client );
@@ -75,6 +79,7 @@ public static function activate() {
7579
'rebelboost_purge_on_comment' => '1',
7680
'rebelboost_surrogate_keys' => '1',
7781
'rebelboost_category_header' => 'X-RM-Categories',
82+
'rebelboost_mode' => 'integration',
7883
);
7984

8085
foreach ( $defaults as $key => $value ) {

includes/class-settings.php

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,21 @@ public function add_menu_page() {
2727
}
2828

2929
public function register_settings() {
30+
// Plugin mode section.
31+
add_settings_section(
32+
'rebelboost_mode_section',
33+
__( 'Plugin Mode', 'rebelboost' ),
34+
array( $this, 'render_mode_section' ),
35+
'rebelboost'
36+
);
37+
38+
add_settings_field( 'rebelboost_mode', __( 'Operating Mode', 'rebelboost' ), array( $this, 'render_mode_field' ), 'rebelboost', 'rebelboost_mode_section' );
39+
40+
register_setting( 'rebelboost_settings', 'rebelboost_mode', array(
41+
'type' => 'string',
42+
'sanitize_callback' => array( $this, 'sanitize_mode' ),
43+
) );
44+
3045
// Connection section.
3146
add_settings_section(
3247
'rebelboost_connection',
@@ -109,6 +124,7 @@ public function render_settings_page() {
109124

110125
<hr>
111126

127+
<?php if ( 'proxy' !== get_option( 'rebelboost_mode', 'integration' ) ) : ?>
112128
<h2><?php esc_html_e( 'DNS Setup Guide', 'rebelboost' ); ?></h2>
113129
<div class="rebelboost-dns-guide">
114130
<p><?php esc_html_e( 'To enable RebelBoost optimization, point your domain to the RebelBoost proxy:', 'rebelboost' ); ?></p>
@@ -119,6 +135,10 @@ public function render_settings_page() {
119135
<li><?php esc_html_e( 'Verify the connection using the "Test Connection" button above.', 'rebelboost' ); ?></li>
120136
</ol>
121137
</div>
138+
<?php else : ?>
139+
<h2><?php esc_html_e( 'Proxy Mode Active', 'rebelboost' ); ?></h2>
140+
<p><?php esc_html_e( 'RebelBoost is operating in proxy mode. Asset URLs are being rewritten to route through the RebelBoost CDN. No DNS or CDN changes are required.', 'rebelboost' ); ?></p>
141+
<?php endif; ?>
122142

123143
<hr>
124144

@@ -221,6 +241,32 @@ public function render_category_header_field() {
221241

222242
// Sanitizers.
223243

244+
public function render_mode_section() {
245+
echo '<p>' . esc_html__( 'Choose how RebelBoost connects to your site.', 'rebelboost' ) . '</p>';
246+
}
247+
248+
public function render_mode_field() {
249+
$current = get_option( 'rebelboost_mode', 'integration' );
250+
?>
251+
<fieldset>
252+
<label style="display:block; margin-bottom:8px;">
253+
<input type="radio" name="rebelboost_mode" value="integration" <?php checked( $current, 'integration' ); ?>>
254+
<strong><?php esc_html_e( 'Integration', 'rebelboost' ); ?></strong>
255+
&mdash; <?php esc_html_e( 'Works with your existing CDN. Handles cache invalidation and surrogate keys. Requires CDN/DNS pointing to RebelBoost.', 'rebelboost' ); ?>
256+
</label>
257+
<label style="display:block;">
258+
<input type="radio" name="rebelboost_mode" value="proxy" <?php checked( $current, 'proxy' ); ?>>
259+
<strong><?php esc_html_e( 'Proxy', 'rebelboost' ); ?></strong>
260+
&mdash; <?php esc_html_e( 'Routes assets through RebelBoost via WordPress output rewriting. No DNS or CDN changes needed.', 'rebelboost' ); ?>
261+
</label>
262+
</fieldset>
263+
<?php
264+
}
265+
266+
public function sanitize_mode( $value ) {
267+
return in_array( $value, array( 'integration', 'proxy' ), true ) ? $value : 'integration';
268+
}
269+
224270
public function sanitize_host( $value ) {
225271
$value = trim( $value );
226272
if ( empty( $value ) ) {
@@ -250,11 +296,36 @@ public function ajax_test_connection() {
250296
if ( ! empty( $_POST['host'] ) ) {
251297
update_option( 'rebelboost_host', $this->sanitize_host( wp_unslash( $_POST['host'] ) ) );
252298
}
299+
if ( isset( $_POST['mode'] ) ) {
300+
update_option( 'rebelboost_mode', $this->sanitize_mode( wp_unslash( $_POST['mode'] ) ) );
301+
}
253302

254303
$this->api_client->reload();
255304

256-
// Register the origin first so the host has an origin config
257-
// before we attempt a purge-based connection test.
305+
$mode = get_option( 'rebelboost_mode', 'integration' );
306+
307+
if ( 'proxy' === $mode ) {
308+
// In proxy mode, verify the API key first.
309+
$result = $this->api_client->test_connection( true );
310+
311+
if ( true !== $result ) {
312+
wp_send_json_error( array( 'message' => $result->get_error_message() ) );
313+
return;
314+
}
315+
316+
// Still register the origin so the service knows where to
317+
// fetch content from — but don't fail if it errors (the host
318+
// may not have a full CDN config yet in proxy mode).
319+
$this->api_client->register_origin();
320+
321+
wp_send_json_success( array(
322+
'message' => __( 'Connected! Proxy mode active — asset URLs will be rewritten.', 'rebelboost' ),
323+
) );
324+
return;
325+
}
326+
327+
// Integration mode: register the origin first so the host has an
328+
// origin config before we attempt a purge-based connection test.
258329
$origin_result = $this->api_client->register_origin();
259330
if ( true !== $origin_result ) {
260331
wp_send_json_error( array( 'message' => $origin_result->get_error_message() ) );

0 commit comments

Comments
 (0)