diff --git a/admin/settings/server.php b/admin/settings/server.php
index aee096179a3..d4f25635ee5 100644
--- a/admin/settings/server.php
+++ b/admin/settings/server.php
@@ -172,6 +172,11 @@ if ($hassiteconfig) {
new lang_string('configproxypassword', 'admin'), ''));
$temp->add(new admin_setting_configtext('proxybypass', new lang_string('proxybypass', 'admin'),
new lang_string('configproxybypass', 'admin'), 'localhost, 127.0.0.1'));
+ $temp->add(new admin_setting_configcheckbox('proxylogunsafe', new lang_string('proxylogunsafe', 'admin'),
+ new lang_string('configproxylogunsafe_help', 'admin'), 0));
+ $temp->add(new admin_setting_configcheckbox('proxyfixunsafe', new lang_string('proxyfixunsafe', 'admin'),
+ new lang_string('configproxyfixunsafe_help', 'admin'), 0));
+
$ADMIN->add('server', $temp);
$temp = new admin_settingpage('maintenancemode', new lang_string('sitemaintenancemode', 'admin'));
diff --git a/lang/en/admin.php b/lang/en/admin.php
index bcae00dd762..e3a56980622 100644
--- a/lang/en/admin.php
+++ b/lang/en/admin.php
@@ -326,6 +326,8 @@ $string['configproxypassword'] = 'Password needed to access internet through pro
$string['configproxyport'] = 'If this server needs to use a proxy computer, then provide the proxy port here.';
$string['configproxytype'] = 'Type of web proxy (PHP5 and cURL extension required for SOCKS5 support).';
$string['configproxyuser'] = 'Username needed to access internet through proxy if required, empty if none (PHP cURL extension required).';
+$string['configproxyfixunsafe_help'] = 'This attempts to fix internal calls which do not go through the proxy by adding the MoodleBot User Agent and using the proxy.';
+$string['configproxylogunsafe_help'] = 'This attempts to log internal calls which do not go through the proxy and should.';
$string['configrecaptchaprivatekey'] = 'String of characters (secret key) used to communicate between your Moodle server and the recaptcha server. ReCAPTCHA keys can be obtained from Google reCAPTCHA.';
$string['configrecaptchapublickey'] = 'String of characters (site key) used to display the reCAPTCHA element in the signup form and site support form. ReCAPTCHA keys can be obtained from Google reCAPTCHA.';
$string['configrequestedstudentname'] = 'Word for student used in requested courses';
@@ -1071,6 +1073,8 @@ $string['proxypassword'] = 'Proxy password';
$string['proxyport'] = 'Proxy port';
$string['proxytype'] = 'Proxy type';
$string['proxyuser'] = 'Proxy username';
+$string['proxylogunsafe'] = 'Log unproxied calls';
+$string['proxyfixunsafe'] = 'Fix unproxied calls';
$string['query'] = 'Query';
$string['question'] = 'Question';
$string['questionbehaviours'] = 'Question behaviours';
diff --git a/lib/filelib.php b/lib/filelib.php
index 044fbfe0f37..8d11e3b6381 100644
--- a/lib/filelib.php
+++ b/lib/filelib.php
@@ -3052,6 +3052,38 @@ function file_is_svg_image_from_mimetype(string $mimetype): bool {
return preg_match('|^image/svg|', $mimetype);
}
+/**
+ * Returns the moodle proxy configuration as a formatted url
+ *
+ * @return string the string to use for proxy settings.
+ */
+function get_moodle_proxy_url() {
+ global $CFG;
+ $proxy = '';
+ if (empty($CFG->proxytype)) {
+ return $proxy;
+ }
+ if (empty($CFG->proxyhost)) {
+ return $proxy;
+ }
+ if ($CFG->proxytype === 'SOCKS5') {
+ // If it is a SOCKS proxy, append the protocol info.
+ $protocol = 'socks5://';
+ } else {
+ $protocol = '';
+ }
+ $proxy = $CFG->proxyhost;
+ if (!empty($CFG->proxyport)) {
+ $proxy .= ':'. $CFG->proxyport;
+ }
+ if (!empty($CFG->proxyuser) && !empty($CFG->proxypassword)) {
+ $proxy = $protocol . $CFG->proxyuser . ':' . $CFG->proxypassword . '@' . $proxy;
+ }
+ return $proxy;
+}
+
+
+
/**
* RESTful cURL class
*
diff --git a/lib/setup.php b/lib/setup.php
index 306a9642f77..a5a22fda346 100644
--- a/lib/setup.php
+++ b/lib/setup.php
@@ -828,6 +828,35 @@ if (empty($CFG->sessiontimeoutwarning)) {
}
\core\session\manager::start();
+if (!empty($CFG->proxylogunsafe) || !empty($CFG->proxyfixunsafe)) {
+ if (!empty($CFG->proxyfixunsafe)) {
+ require_once($CFG->libdir.'/filelib.php');
+
+ $proxyurl = get_moodle_proxy_url();
+ // This fixes stream handlers inside php.
+ $defaults = stream_context_set_default([
+ 'http' => [
+ 'user_agent' => \core_useragent::get_moodlebot_useragent(),
+ 'proxy' => $proxyurl
+ ],
+ ]);
+
+ // Attempt to tell other web clients to use the proxy too. This only
+ // works for clients written in php in the same process, it will not
+ // work for with requests done in another process from an exec call.
+ putenv('http_proxy=' . $proxyurl);
+ putenv('https_proxy=' . $proxyurl);
+ putenv('HTTPS_PROXY=' . $proxyurl);
+ } else {
+ $defaults = stream_context_get_default();
+ }
+
+ if (!empty($CFG->proxylogunsafe)) {
+ stream_context_set_params($defaults, ['notification' => 'proxy_log_callback']);
+ }
+
+}
+
// Set default content type and encoding, developers are still required to use
// echo $OUTPUT->header() everywhere, anything that gets set later should override these headers.
// This is intended to mitigate some security problems.
diff --git a/lib/setuplib.php b/lib/setuplib.php
index aacdb699948..47e9a90b528 100644
--- a/lib/setuplib.php
+++ b/lib/setuplib.php
@@ -2172,3 +2172,21 @@ class bootstrap_renderer {
return $html;
}
}
+
+/**
+ * Add http stream instrumentation
+ *
+ * This detects which any reads or writes to a php stream which uses
+ * the 'http' handler. Ideally 100% of traffic uses the Moodle curl
+ * libraries which do not use php streams.
+ *
+ * @param array $code stream callback code
+ */
+function proxy_log_callback($code) {
+ if ($code == STREAM_NOTIFY_CONNECT) {
+ $trace = debug_backtrace();
+ $function = $trace[count($trace) - 1];
+ $error = "Unsafe internet IO detected: {$function['function']} with arguments " . join(', ', $function['args']) . "\n";
+ error_log($error . format_backtrace($trace, true)); // phpcs:ignore
+ }
+}
diff --git a/lib/upgrade.txt b/lib/upgrade.txt
index 2d4524d1d17..b19f948aafb 100644
--- a/lib/upgrade.txt
+++ b/lib/upgrade.txt
@@ -21,6 +21,7 @@ information provided here is intended especially for developers.
Moodle version the plugin is incompatible with but the implemented logic for the check was the opposite. Plugins declaring this
attribute may encounter different behaviours between older Moodle versions (proxylogunsafe and proxyfixunsafe to detect code which doesn't honor the proxy config
=== 4.0 ===