-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathwoo-secure-proxy.php
More file actions
223 lines (196 loc) · 6.34 KB
/
woo-secure-proxy.php
File metadata and controls
223 lines (196 loc) · 6.34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
<?php
/**
* Plugin Name: WooSecureProxy
* Plugin URI: https://github.com/Alfrahi/woosecureproxy
* Description: Secure API proxy for WooCommerce with HMAC authentication, replay attack protection (nonce + timestamp), per-endpoint rate limiting, request size limits, and strict JSON schema validation.
* Version: 1.0.0
* Author: Abdullah Alfrahi
* Author URI: https://alfrahi.com
* License: GPL-3.0-or-later
* License URI: https://www.gnu.org/licenses/gpl-3.0.html
* Text Domain: woo-secure-proxy
* Domain Path: /languages
* Requires PHP: 7.4
* Requires at least: 6.0
* Tested up to: 6.7
* WC requires at least: 8.0
* WC tested up to: 10.3
*
* @package WooSecureProxy
* @copyright 2025 Abdullah Alfrahi
* @license GPL-3.0-or-later
*/
/**
* Main plugin bootstrap file.
*
* This file serves as the entry point for the WooSecureProxy plugin.
* It performs security checks, defines essential constants, sets up autoloading,
* declares WooCommerce compatibility, and initializes the main plugin instance.
*
* @package WooSecureProxy
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Ensures the plugin is completely disabled if a strong PROXY_SECRET is not defined.
* A minimum of 32 characters is enforced for cryptographic security.
*/
if ( ! defined( 'PROXY_SECRET' ) || strlen( (string) PROXY_SECRET ) < 32 ) {
define( 'WSP_DISABLED', true );
/**
* Displays an admin notice explaining why the plugin is disabled and how to fix it.
*/
add_action(
'admin_notices',
function () {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
// Generate a random PROXY_SECRET.
$proxy_secret = substr( bin2hex( random_bytes( 32 ) ), 0, 64 );
// Escape the PROXY_SECRET to prevent any issues.
$escaped_proxy_secret = esc_html( $proxy_secret );
// Create the HTML output, escaping all dynamic parts.
$message = sprintf(
'<div class="notice notice-error is-dismissible"><p>
<strong>WooSecureProxy: PLUGIN DISABLED</strong><br>
You <strong>must</strong> define a strong <code>PROXY_SECRET</code> (min 32 chars) in wp-config.php<br>
Example: <code>define("PROXY_SECRET", "%s");</code><br>
Also, it is recommended to add WooCommerce consumer key and secret like these examples:<br>
<code>define("WC_CONSUMER_KEY", "ck_...");</code><br>
<code>define("WC_CONSUMER_SECRET", "cs_...");</code>
</p></div>',
$escaped_proxy_secret
);
// Echo the safely escaped output.
echo wp_kses_post( $message );
}
);
return;
}
/** Absolute path to the main plugin file */
define( 'WSP_PLUGIN_FILE', __FILE__ );
/** Plugin basename (used for assets, hooks, etc.) */
define( 'WSP_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
/** Current plugin version */
define( 'WSP_VERSION', '1.0.0' );
/** Filesystem path to the plugin directory */
define( 'WSP_PATH', plugin_dir_path( __FILE__ ) );
/** URL to the plugin directory */
define( 'WSP_URL', plugin_dir_url( __FILE__ ) );
/** Allowed clock skew for timestamp validation (seconds) */
define( 'PROXY_TIMESTAMP_SKEW', defined( 'PROXY_TIMESTAMP_SKEW' ) ? PROXY_TIMESTAMP_SKEW : 300 );
/** Maximum allowed request body size in bytes (default 512 KB) */
define( 'PROXY_MAX_BODY_SIZE', defined( 'PROXY_MAX_BODY_SIZE' ) ? PROXY_MAX_BODY_SIZE : 512 * 1024 );
/** Time-to-live for used nonces (prevents replay attacks) */
define( 'PROXY_NONCE_TTL', defined( 'PROXY_NONCE_TTL' ) ? PROXY_NONCE_TTL : 600 );
/**
* Default rate limit configuration per endpoint.
*
* Structure:
* - 'ip' : requests per IP per window
* - 'app' : requests per authenticated app per window
* - 'win' : time window in seconds
*/
global $wsp_default_rate_limits;
$wsp_default_rate_limits = array(
'default' => array(
'ip' => 120,
'app' => 5000,
'win' => 60,
),
'createOrder' => array(
'ip' => 15,
'app' => 300,
'win' => 60,
),
'updateOrder' => array(
'ip' => 30,
'app' => 600,
'win' => 60,
),
'getProducts' => array(
'ip' => 200,
'app' => 10000,
'win' => 60,
),
);
/**
* Declares compatibility with WooCommerce Custom Order Tables (HPOS).
*/
add_action(
'before_woocommerce_init',
function () {
if ( class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) {
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true );
}
}
);
/**
* Sets up Composer autoloading if available; falls back to a simple PSR-4-style autoloader.
*/
if ( file_exists( WSP_PATH . 'vendor/autoload.php' ) ) {
require_once WSP_PATH . 'vendor/autoload.php';
} else {
spl_autoload_register(
function ( $class_name ) {
if ( strpos( $class_name, 'WooSecureProxy\\' ) !== 0 ) {
return;
}
$file = WSP_PATH . 'src/' . str_replace( '\\', '/', substr( $class_name, 15 ) ) . '.php';
if ( file_exists( $file ) ) {
require $file;
}
}
);
}
/**
* Warns administrators if the required firebase/php-jwt library is missing.
*/
add_action(
'admin_notices',
function () {
if ( ! class_exists( 'Firebase\JWT\JWT' ) ) {
echo '<div class="notice notice-error is-dismissible"><p>
<strong>WooSecureProxy:</strong> Missing required dependency
<code>firebase/php-jwt</code>.
Please run <code>composer install</code> in the plugin directory
or install the library via Composer.
</p></div>';
}
}
);
/**
* Initializes the plugin after all plugins are loaded.
*
* - Checks for WooCommerce
* - Sets up non-persistent cache groups for nonces and rate limiting
* - Loads text domain
* - Starts the main plugin singleton
*/
add_action(
'plugins_loaded',
function () {
if ( ! class_exists( 'WooCommerce' ) ) {
add_action(
'admin_notices',
function () {
echo '<div class="notice notice-error"><p>'
. esc_html__( 'WooSecureProxy requires WooCommerce to be active.', 'woo-secure-proxy' )
. '</p></div>';
}
);
return;
}
if ( wp_cache_supports( 'add_non_persistent_groups' ) ) {
wp_cache_add_non_persistent_groups( array( 'wsp_nonces', 'wsp_rl' ) );
}
load_plugin_textdomain( 'woo-secure-proxy', false, dirname( WSP_PLUGIN_BASENAME ) . '/languages' );
\WooSecureProxy\WooSecureProxy::instance();
}
);
/** Runs on plugin activation */
register_activation_hook( __FILE__, array( \WooSecureProxy\WooSecureProxy::class, 'activate' ) );
/** Runs on plugin deactivation */
register_deactivation_hook( __FILE__, array( \WooSecureProxy\WooSecureProxy::class, 'deactivate' ) );