1: <?php
2: /*
3: * SimpleID
4: *
5: * Copyright (C) Kelvin Mo 2014-2026
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: } elseif (isset($parts['scheme'])) {
243: $scheme = $parts['scheme'];
244: } else {
245: throw new \UnexpectedValueException('Configuration value canonical_base_path does not contain a scheme');
246: }
247:
248: $url = $scheme . '://';
249: if (isset($parts['user'])) {
250: $url .= $parts['user'];
251: if (isset($parts['pass'])) $url .= ':' . $parts['pass'];
252: $url .= '@';
253: }
254: if (isset($parts['host'])) $url .= $parts['host'];
255: if (isset($parts['port'])) $url .= ':' . $parts['port'];
256:
257: return $url;
258: }
259:
260: /**
261: * Gets the origin of a URI
262: *
263: * @param string $uri the URI
264: * @return string the origin
265: * @link https://www.rfc-editor.org/rfc/rfc6454.txt
266: */
267: protected function getOrigin($uri) {
268: $parts = parse_url($uri);
269: if ($parts == false) throw new \UnexpectedValueException('Invalid URL');
270: if (!isset($parts['scheme']) || !isset($parts['host'])) throw new \UnexpectedValueException('Missing scheme or host name');
271:
272: $origin = $parts['scheme'] . '://';
273: $origin .= $parts['host'];
274: if (isset($parts['port'])) $origin .= ':' . $parts['port'];
275:
276: return $origin;
277: }
278:
279: /**
280: * Displays a fatal error message and exits.
281: *
282: * @param string $error the message to set
283: * @param int $code the HTTP status code to send
284: * @return void
285: */
286: protected function fatalError(string $error, int $code = 500) {
287: // These status codes aren't currently included in Fat-Free Framework
288: static $extra_status_codes = [
289: 426 => 'Upgrade Required'
290: ];
291:
292: if (isset($extra_status_codes[$code])) {
293: $title = $extra_status_codes[$code];
294: if (!$this->f3->get('CLI') && !headers_sent())
295: header($_SERVER['SERVER_PROTOCOL'] . ' ' . $code . ' ' . $title);
296: } else {
297: // This also sends the HTTP status code
298: $title = $this->f3->status($code);
299: }
300:
301: $this->f3->expire(-1);
302: $trace = $this->f3->trace();
303:
304: if ($this->f3->get('CLI')) {
305: $exit_code = 1;
306: echo PHP_EOL.'==================================='
307: .PHP_EOL.'ERROR ' . $code . ' - '. $title
308: .PHP_EOL.$error;
309: } else {
310: $exit_code = 0;
311: $this->f3->set('error', $error);
312: if ($this->f3->get('DEBUG') > 0) $this->f3->set('trace', $trace);
313:
314: $this->f3->set('page_class', 'is-dialog-page');
315: $this->f3->set('title', $title);
316: $this->f3->set('layout', 'fatal_error.html');
317:
318: $tpl = Template::instance();
319: print $tpl->render('page.html');
320: }
321: exit($exit_code);
322: }
323:
324: /**
325: * Compares two strings using the same time whether they're equal or not.
326: * This function should be used to mitigate timing attacks when, for
327: * example, comparing password hashes
328: *
329: * @param string $str1
330: * @param string $str2
331: * @return bool true if the two strings are equal
332: */
333: public function secureCompare($str1, $str2) {
334: if (function_exists('hash_equals')) return hash_equals($str1, $str2);
335:
336: $xor = $str1 ^ $str2;
337: $result = strlen($str1) ^ strlen($str2); //not the same length, then fail ($result != 0)
338: for ($i = strlen($xor) - 1; $i >= 0; $i--) $result += ord($xor[$i]);
339: return !$result;
340: }
341: }
342:
343: ?>
344: