1: <?php
2: /*
3: * SimpleID
4: *
5: * Copyright (C) Kelvin Mo 2014-2025
6: *
7: * This program is free software; you can redistribute it and/or
8: * modify it under the terms of the GNU General Public
9: * License as published by the Free Software Foundation; either
10: * version 2 of the License, or (at your option) any later version.
11: *
12: * This program is distributed in the hope that it will be useful,
13: * but WITHOUT ANY WARRANTY; without even the implied warranty of
14: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15: * General Public License for more details.
16: *
17: * You should have received a copy of the GNU General Public
18: * License along with this program; if not, write to the Free
19: * Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
20: *
21: */
22:
23: namespace SimpleID;
24:
25: use SimpleID\Auth\AuthManager;
26: use SimpleID\Util\UI\Template;
27:
28: /**
29: * A SimpleID module.
30: *
31: * A module represents a single unit of functionality in SimpleID.
32: * Apart from a small number of required modules, modules can be enabled
33: * or disabled via the configuration file.
34: *
35: * A SimpleID module is a singleton class under the FatFree Framework.
36: * This class is the superclass of all SimpleID modules. This class
37: * also provides a number of common functions which are used by
38: * all modules.
39: */
40: abstract class Module extends \Prefab {
41: /** FatFree framework object
42: * @var \Base
43: */
44: protected $f3;
45:
46: /** Logger
47: * @var \Psr\Log\LoggerInterface
48: */
49: protected $logger;
50:
51: /**
52: * Initialises the module.
53: *
54: * This static method is called during initialisation. Subclasses can
55: * use this to, among other things:
56: *
57: * - register URL routes with the Fat-Free Framework using `$f3->route()`
58: * or `$f3->map()`
59: * - register events
60: *
61: * @param \Base $f3 the FatFree framework
62: * @return void
63: */
64: public static function init($f3) {
65:
66: }
67:
68: /**
69: * Creates a module.
70: *
71: * This default constructor performs the following:
72: *
73: * - sets the {@link $logger} variable to the current logger
74: * - sets the locale domain
75: */
76: public function __construct() {
77: $this->f3 = \Base::instance();
78: $this->logger = $this->f3->get('logger');
79:
80: $mgr = ModuleManager::instance();
81: $info = $mgr->getModuleInfo(get_class($this));
82: }
83:
84: /**
85: * FatFree Framework event handler.
86: *
87: * This event handler initialises the user system. It starts the PHP session
88: * and loads data for the currently logged-in user, if any.
89: *
90: * @return void
91: */
92: public function beforeroute() {
93: $auth = AuthManager::instance();
94: $auth->initSession();
95: $auth->initUser();
96: }
97:
98: /**
99: * Determines whether the current connection with the user agent is via
100: * HTTPS.
101: *
102: * HTTPS is detected if one of the following occurs:
103: *
104: * - $_SERVER['HTTPS'] is set to 'on' (Apache installations)
105: * - $_SERVER['HTTP_X_FORWARDED_PROTO'] is set to 'https' (reverse proxies)
106: * - $_SERVER['HTTP_FRONT_END_HTTPS'] is set to 'on'
107: *
108: * @return bool true if the connection is via HTTPS
109: */
110: protected function isHttps() {
111: return ($this->f3->get('SCHEME') == 'https')
112: || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && (strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https'))
113: || (isset($_SERVER['HTTP_FRONT_END_HTTPS']) && ($_SERVER['HTTP_FRONT_END_HTTPS'] == 'on'));
114: }
115:
116:
117: /**
118: * Ensure the current connection with the user agent is secure with HTTPS.
119: *
120: * This function uses {@link isHttps()} to determine whether the connection
121: * is via HTTPS. If it is, this function will return successfully.
122: *
123: * If it is not, what happens next is determined by the following steps.
124: *
125: * 1. If $allow_override is true and allow_plaintext is also true,
126: * then the function will return successfully
127: * 2. Otherwise, then it will either redirect (if $action is
128: * redirect) or return an error (if $action is error)
129: *
130: * @param string $action what to do if connection is not secure - either
131: * 'redirect' or 'error'
132: * @param boolean $allow_override whether allow_plaintext is checked
133: * to see if an unencrypted connection is allowed
134: * @param string $redirect_url if $action is redirect, what URL to redirect to.
135: * If null, this will redirect to the same page (albeit with an HTTPS connection)
136: * @param boolean $strict whether HTTP Strict Transport Security is active
137: * @return void
138: */
139: protected function checkHttps($action = 'redirect', $allow_override = false, $redirect_url = null, $strict = true) {
140: if ($this->f3->get('CLI')) return;
141:
142: if ($this->isHttps()) {
143: if ($strict) header('Strict-Transport-Security: max-age=3600');
144: return;
145: }
146:
147: $config = $this->f3->get('config');
148:
149: if ($allow_override && $config['allow_plaintext']) return;
150:
151: if ($action == 'error') {
152: header('Upgrade: TLS/1.2, HTTP/1.1');
153: header('Connection: Upgrade');
154: $this->fatalError($this->f3->get('intl.common.require_https'), 426);
155: exit;
156: }
157:
158: if ($redirect_url == null) $redirect_url = $this->getCanonicalURL($this->f3->get('PATH'), $this->f3->get('SERVER.QUERY_STRING'), 'https');
159:
160: $this->f3->status(301);
161: header('Location: ' . $redirect_url);
162: exit;
163: }
164:
165: /**
166: * Obtains a SimpleID URL. URLs produced by SimpleID should use this function.
167: *
168: * @param string $path the FatFree path or alias
169: * @param string $query a properly encoded query string
170: * @param string $secure one of 'https' to force an HTTPS connection, 'http' to force
171: * an unencrypted HTTP connection, 'detect' to base on the current connection, or NULL to vary based on the
172: * `canonical_base_path` configuration
173: * @return string the url
174: *
175: * @since 0.7
176: */
177: public function getCanonicalURL($path = '', $query = '', $secure = null) {
178: $config = $this->f3->get('config');
179: $canonical_base_path = $config['canonical_base_path'];
180:
181: if (preg_match('/^(?:@(\w+)(?:(\(.+?)\))*|https?:\/\/)/', $path, $parts, PREG_UNMATCHED_AS_NULL)) {
182: if (isset($parts[1])) {
183: $aliases = $this->f3->get('ALIASES');
184:
185: if (!empty($aliases[$parts[1]])) {
186: $path = $aliases[$parts[1]];
187: $path = $this->f3->build($path, isset($parts[2]) ? $this->f3->parse($parts[2]) : []);
188: $path = ltrim($path, '/');
189: }
190: }
191: }
192:
193: // Make sure that the base has a trailing slash
194: if ((substr($config['canonical_base_path'], -1) == '/')) {
195: $url = $config['canonical_base_path'];
196: } else {
197: $url = $config['canonical_base_path'] . '/';
198: }
199:
200: if (($secure == 'https') && (stripos($url, 'http:') === 0)) {
201: $url = 'https:' . substr($url, 5);
202: }
203: if (($secure == 'http') && (stripos($url, 'https:') === 0)) {
204: $url = 'http:' . substr($url, 6);
205: }
206: if (($secure == 'detect') && ($this->isHttps()) && (stripos($url, 'http:') === 0)) {
207: $url = 'https:' . substr($url, 5);
208: }
209: if (($secure == 'detect') && (!$this->isHttps()) && (stripos($url, 'https:') === 0)) {
210: $url = 'http:' . substr($url, 6);
211: }
212:
213: $url .= $path . (($query == '') ? '' : '?' . $query);
214:
215: return $url;
216: }
217:
218: /**
219: * Obtains the SimpleID host URL.
220: *
221: * This function returns the scheme, host name, port, user name and password (if specified) from
222: * the `canonical_base_path` configuration variable. It is used, among other things, as the
223: * issuer identifier for JWTs issued by this installation.
224: *
225: * @param string $secure one of 'https' to force an HTTPS connection, 'http' to force
226: * an unencrypted HTTP connection, 'detect' to base on the current connection, or NULL to vary based on the
227: * `canonical_base_path` configuration
228: * @return string the url
229: *
230: */
231: public function getCanonicalHost($secure = null) {
232: $config = $this->f3->get('config');
233: $canonical_base_path = $config['canonical_base_path'];
234:
235: $parts = parse_url($canonical_base_path);
236: if ($parts == false) return $canonical_base_path;
237:
238: if ($secure == 'https') {
239: $scheme = 'https';
240: } elseif ($secure == 'http') {
241: $scheme = 'http';
242: } else {
243: $scheme = $parts['scheme'];
244: }
245:
246: $url = $scheme . '://';
247: if (isset($parts['user'])) {
248: $url .= $parts['user'];
249: if (isset($parts['pass'])) $url .= ':' . $parts['pass'];
250: $url .= '@';
251: }
252: if (isset($parts['host'])) $url .= $parts['host'];
253: if (isset($parts['port'])) $url .= ':' . $parts['port'];
254:
255: return $url;
256: }
257:
258: /**
259: * Gets the origin of a URI
260: *
261: * @param string $uri the URI
262: * @return string the origin
263: * @link https://www.rfc-editor.org/rfc/rfc6454.txt
264: */
265: protected function getOrigin($uri) {
266: $parts = parse_url($uri);
267: if ($parts == false) return $uri;
268:
269: $origin = $parts['scheme'] . '://';
270: $origin .= $parts['host'];
271: if (isset($parts['port'])) $origin .= ':' . $parts['port'];
272:
273: return $origin;
274: }
275:
276: /**
277: * Displays a fatal error message and exits.
278: *
279: * @param string $error the message to set
280: * @param int $code the HTTP status code to send
281: * @return void
282: */
283: protected function fatalError(string $error, int $code = 500) {
284: // These status codes aren't currently included in Fat-Free Framework
285: static $extra_status_codes = [
286: 426 => 'Upgrade Required'
287: ];
288:
289: if (isset($extra_status_codes[$code])) {
290: $title = $extra_status_codes[$code];
291: if (!$this->f3->get('CLI') && !headers_sent())
292: header($_SERVER['SERVER_PROTOCOL'] . ' ' . $code . ' ' . $title);
293: } else {
294: // This also sends the HTTP status code
295: $title = $this->f3->status($code);
296: }
297:
298: $this->f3->expire(-1);
299: $trace = $this->f3->trace();
300:
301: if ($this->f3->get('CLI')) {
302: $exit_code = 1;
303: echo PHP_EOL.'==================================='
304: .PHP_EOL.'ERROR ' . $code . ' - '. $title
305: .PHP_EOL.$error;
306: } else {
307: $exit_code = 0;
308: $this->f3->set('error', $error);
309: if ($this->f3->get('DEBUG') > 0) $this->f3->set('trace', $trace);
310:
311: $this->f3->set('page_class', 'is-dialog-page');
312: $this->f3->set('title', $title);
313: $this->f3->set('layout', 'fatal_error.html');
314:
315: $tpl = Template::instance();
316: print $tpl->render('page.html');
317: }
318: exit($exit_code);
319: }
320:
321: /**
322: * Compares two strings using the same time whether they're equal or not.
323: * This function should be used to mitigate timing attacks when, for
324: * example, comparing password hashes
325: *
326: * @param string $str1
327: * @param string $str2
328: * @return bool true if the two strings are equal
329: */
330: public function secureCompare($str1, $str2) {
331: if (function_exists('hash_equals')) return hash_equals($str1, $str2);
332:
333: $xor = $str1 ^ $str2;
334: $result = strlen($str1) ^ strlen($str2); //not the same length, then fail ($result != 0)
335: for ($i = strlen($xor) - 1; $i >= 0; $i--) $result += ord($xor[$i]);
336: return !$result;
337: }
338: }
339:
340: ?>
341: