Skip to content

Commit 1a364e2

Browse files
committed
2 parents 2f1e506 + 57118bc commit 1a364e2

File tree

7 files changed

+599
-80
lines changed

7 files changed

+599
-80
lines changed

docs/img/device_details.png

31.1 KB
Loading

front/index.php

Lines changed: 146 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -3,76 +3,155 @@
33

44
<?php
55

6-
//------------------------------------------------------------------------------
7-
// check if authenticated
8-
// Be CAREFUL WHEN INCLUDING NEW PHP FILES
9-
require_once $_SERVER['DOCUMENT_ROOT'] . '/php/server/db.php';
10-
require_once $_SERVER['DOCUMENT_ROOT'] . '/php/templates/language/lang.php';
11-
require_once $_SERVER['DOCUMENT_ROOT'] . '/php/templates/security.php';
12-
13-
$CookieSaveLoginName = 'NetAlertX_SaveLogin';
14-
15-
if ($nax_WebProtection != 'true')
16-
{
17-
header('Location: devices.php');
18-
$_SESSION["login"] = 1;
6+
require_once $_SERVER['DOCUMENT_ROOT'].'/php/server/db.php';
7+
require_once $_SERVER['DOCUMENT_ROOT'].'/php/templates/language/lang.php';
8+
require_once $_SERVER['DOCUMENT_ROOT'].'/php/templates/security.php';
9+
10+
// if (session_status() === PHP_SESSION_NONE) {
11+
// session_start();
12+
// }
13+
14+
session_start();
15+
16+
const DEFAULT_REDIRECT = '/devices.php';
17+
18+
/* =====================================================
19+
Helper Functions
20+
===================================================== */
21+
22+
function safe_redirect(string $path): void {
23+
header("Location: {$path}", true, 302);
1924
exit;
2025
}
2126

22-
// Logout
23-
if (isset ($_GET["action"]) && $_GET["action"] == 'logout')
24-
{
25-
setcookie($CookieSaveLoginName, '', time()+1); // reset cookie
26-
$_SESSION["login"] = 0;
27-
header('Location: index.php');
28-
exit;
27+
function validate_local_path(?string $encoded): string {
28+
if (!$encoded) return DEFAULT_REDIRECT;
29+
30+
$decoded = base64_decode($encoded, true);
31+
if ($decoded === false) {
32+
return DEFAULT_REDIRECT;
33+
}
34+
35+
// strict local path check (allow safe query strings + fragments)
36+
// Using ~ as the delimiter instead of #
37+
if (!preg_match('~^(?!//)(?!.*://)/[a-zA-Z0-9_\-./?=&:%#]*$~', $decoded)) {
38+
return DEFAULT_REDIRECT;
39+
}
40+
41+
return $decoded;
42+
}
43+
44+
function extract_hash_from_path(string $path): array {
45+
/*
46+
Split a path into path and hash components.
47+
48+
For deep links encoded in the 'next' parameter like /devices.php#device-123,
49+
extract the hash fragment so it can be properly included in the redirect.
50+
51+
Args:
52+
path: Full path potentially with hash (e.g., "/devices.php#device-123")
53+
54+
Returns:
55+
Array with keys 'path' (without hash) and 'hash' (with # prefix, or empty string)
56+
*/
57+
$parts = explode('#', $path, 2);
58+
return [
59+
'path' => $parts[0],
60+
'hash' => !empty($parts[1]) ? '#' . $parts[1] : ''
61+
];
62+
}
63+
64+
function append_hash(string $url): string {
65+
// First check if the URL already has a hash from the deep link
66+
$parts = extract_hash_from_path($url);
67+
if (!empty($parts['hash'])) {
68+
return $parts['path'] . $parts['hash'];
69+
}
70+
71+
// Fall back to POST url_hash (for browser-captured hashes)
72+
if (!empty($_POST['url_hash'])) {
73+
$sanitized = preg_replace('/[^#a-zA-Z0-9_\-]/', '', $_POST['url_hash']);
74+
if (str_starts_with($sanitized, '#')) {
75+
return $url . $sanitized;
76+
}
77+
}
78+
return $url;
2979
}
3080

31-
// Password without Cookie check -> pass and set initial cookie
32-
if (isset ($_POST["loginpassword"]) && $nax_Password === hash('sha256',$_POST["loginpassword"]))
33-
{
34-
header('Location: devices.php');
35-
$_SESSION["login"] = 1;
36-
if (isset($_POST['PWRemember'])) {setcookie($CookieSaveLoginName, hash('sha256',$_POST["loginpassword"]), time()+604800);}
81+
function is_authenticated(): bool {
82+
return isset($_SESSION['login']) && $_SESSION['login'] === 1;
3783
}
3884

39-
// active Session or valid cookie (cookie not extends)
40-
if (( isset ($_SESSION["login"]) && ($_SESSION["login"] == 1)) || (isset ($_COOKIE[$CookieSaveLoginName]) && $nax_Password === $_COOKIE[$CookieSaveLoginName]))
41-
{
42-
header('Location: devices.php');
43-
$_SESSION["login"] = 1;
44-
if (isset($_POST['PWRemember'])) {setcookie($CookieSaveLoginName, hash('sha256',$_POST["loginpassword"]), time()+604800);}
85+
function login_user(): void {
86+
$_SESSION['login'] = 1;
87+
session_regenerate_id(true);
4588
}
4689

90+
91+
function logout_user(): void {
92+
$_SESSION = [];
93+
session_destroy();
94+
}
95+
96+
/* =====================================================
97+
Redirect Handling
98+
===================================================== */
99+
100+
$redirectTo = validate_local_path($_GET['next'] ?? null);
101+
102+
/* =====================================================
103+
Web Protection Disabled
104+
===================================================== */
105+
106+
if ($nax_WebProtection !== 'true') {
107+
if (!is_authenticated()) {
108+
login_user();
109+
}
110+
safe_redirect(append_hash($redirectTo));
111+
}
112+
113+
/* =====================================================
114+
Login Attempt
115+
===================================================== */
116+
117+
if (!empty($_POST['loginpassword'])) {
118+
119+
$incomingHash = hash('sha256', $_POST['loginpassword']);
120+
121+
if (hash_equals($nax_Password, $incomingHash)) {
122+
123+
login_user();
124+
125+
// Redirect to target page, preserving deep link hash if present
126+
safe_redirect(append_hash($redirectTo));
127+
}
128+
}
129+
130+
/* =====================================================
131+
Already Logged In
132+
===================================================== */
133+
134+
if (is_authenticated()) {
135+
safe_redirect(append_hash($redirectTo));
136+
}
137+
138+
/* =====================================================
139+
Login UI Variables
140+
===================================================== */
141+
47142
$login_headline = lang('Login_Toggle_Info_headline');
48-
$login_info = lang('Login_Info');
49-
$login_mode = 'danger';
50-
$login_display_mode = 'display: block;';
51-
$login_icon = 'fa-info';
52-
53-
// no active session, cookie not checked
54-
if (isset ($_SESSION["login"]) == FALSE || $_SESSION["login"] != 1)
55-
{
56-
if ($nax_Password === '8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92')
57-
{
143+
$login_info = lang('Login_Info');
144+
$login_mode = 'info';
145+
$login_display_mode = 'display:none;';
146+
$login_icon = 'fa-info';
147+
148+
if ($nax_Password === '8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92') {
58149
$login_info = lang('Login_Default_PWD');
59150
$login_mode = 'danger';
60-
$login_display_mode = 'display: block;';
151+
$login_display_mode = 'display:block;';
61152
$login_headline = lang('Login_Toggle_Alert_headline');
62153
$login_icon = 'fa-ban';
63-
}
64-
else
65-
{
66-
$login_mode = 'info';
67-
$login_display_mode = 'display: none;';
68-
$login_headline = lang('Login_Toggle_Info_headline');
69-
$login_icon = 'fa-info';
70-
}
71154
}
72-
73-
// ##################################################
74-
// ## Login Processing end
75-
// ##################################################
76155
?>
77156

78157
<!DOCTYPE html>
@@ -109,27 +188,21 @@
109188
<!-- /.login-logo -->
110189
<div class="login-box-body">
111190
<p class="login-box-msg"><?= lang('Login_Box');?></p>
112-
<form action="index.php" method="post">
191+
<form action="index.php<?php
192+
echo !empty($_GET['next'])
193+
? '?next=' . htmlspecialchars($_GET['next'], ENT_QUOTES, 'UTF-8')
194+
: '';
195+
?>" method="post">
113196
<div class="form-group has-feedback">
197+
<input type="hidden" name="url_hash" id="url_hash">
114198
<input type="password" class="form-control" placeholder="<?= lang('Login_Psw-box');?>" name="loginpassword">
115199
<span class="glyphicon glyphicon-lock form-control-feedback"></span>
116200
</div>
117201
<div class="row">
118-
<div class="col-xs-8">
119-
<div class="checkbox icheck">
120-
<label>
121-
<input type="checkbox" name="PWRemember">
122-
<div style="margin-left: 10px; display: inline-block; vertical-align: top;">
123-
<?= lang('Login_Remember');?><br><span style="font-size: smaller"><?= lang('Login_Remember_small');?></span>
124-
</div>
125-
</label>
126-
</div>
127-
</div>
128-
<!-- /.col -->
129-
<div class="col-xs-4" style="padding-top: 10px;">
202+
<div class="col-xs-12">
130203
<button type="submit" class="btn btn-primary btn-block btn-flat"><?= lang('Login_Submit');?></button>
131204
</div>
132-
<!-- /.col -->
205+
<!-- /.col -->
133206
</div>
134207
</form>
135208

@@ -159,6 +232,9 @@
159232
<!-- iCheck -->
160233
<script src="lib/iCheck/icheck.min.js"></script>
161234
<script>
235+
if (window.location.hash) {
236+
document.getElementById('url_hash').value = window.location.hash;
237+
}
162238
$(function () {
163239
$('input').iCheck({
164240
checkboxClass: 'icheckbox_square-blue',
@@ -174,7 +250,7 @@ function Passwordhinfo() {
174250
} else {
175251
x.style.display = "none";
176252
}
177-
}
253+
}
178254

179255
</script>
180256
</body>

front/php/templates/security.php

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
<?php
22

3+
// Start session if not already started
4+
if (session_status() == PHP_SESSION_NONE) {
5+
session_start();
6+
}
7+
38
// Constants
49
$configFolderPath = rtrim(getenv('NETALERTX_CONFIG') ?: '/data/config', '/');
510
$legacyConfigPath = $_SERVER['DOCUMENT_ROOT'] . "/../config/app.conf";
@@ -45,10 +50,6 @@ function redirect($url) {
4550
$authHeader = apache_request_headers()['Authorization'] ?? '';
4651
$sessionLogin = isset($_SESSION['login']) ? $_SESSION['login'] : 0;
4752

48-
// Start session if not already started
49-
if (session_status() == PHP_SESSION_NONE) {
50-
session_start();
51-
}
5253

5354
// Handle logout
5455
if (!empty($_REQUEST['action']) && $_REQUEST['action'] == 'logout') {
@@ -82,11 +83,12 @@ function redirect($url) {
8283
$isLoggedIn = isset($_SESSION['login']) && $_SESSION['login'] == 1;
8384

8485
// Determine if the user should be redirected
85-
if ($isLoggedIn || $isLogonPage || (isset($_COOKIE[COOKIE_SAVE_LOGIN_NAME]) && $nax_Password === $_COOKIE[COOKIE_SAVE_LOGIN_NAME])) {
86+
if ($isLoggedIn || $isLogonPage) {
8687
// Logged in or stay on this page if we are on the index.php already
8788
} else {
8889
// We need to redirect
89-
redirect('/index.php');
90+
$returnUrl = rawurlencode(base64_encode($_SERVER['REQUEST_URI']));
91+
redirect("/index.php?next=" . $returnUrl);
9092
exit; // exit is needed to prevent authentication bypass
9193
}
9294
}

server/api_server/api_server_start.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@
7575
BaseResponse, DeviceTotalsResponse,
7676
DeviceTotalsNamedResponse,
7777
EventsTotalsNamedResponse,
78-
DeleteDevicesRequest, DeviceImportRequest,
78+
DeleteDevicesRequest,
7979
DeviceImportResponse, UpdateDeviceColumnRequest,
8080
LockDeviceFieldRequest, UnlockDeviceFieldsRequest,
8181
CopyDeviceRequest, TriggerScanRequest,
@@ -94,7 +94,7 @@
9494
DbQueryRequest, DbQueryResponse,
9595
DbQueryUpdateRequest, DbQueryDeleteRequest,
9696
AddToQueueRequest, GetSettingResponse,
97-
RecentEventsRequest, SetDeviceAliasRequest
97+
RecentEventsRequest, SetDeviceAliasRequest,
9898
)
9999

100100
from .sse_endpoint import ( # noqa: E402 [flake8 lint suppression]
@@ -1933,6 +1933,9 @@ def check_auth(payload=None):
19331933
return jsonify({"success": True, "message": "Authentication check successful"}), 200
19341934

19351935

1936+
# Remember Me is now implemented via cookies only (no API endpoints required)
1937+
1938+
19361939
# --------------------------
19371940
# Health endpoint
19381941
# --------------------------

server/api_server/openapi/schemas.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1034,8 +1034,6 @@ class GetSettingResponse(BaseResponse):
10341034
# =============================================================================
10351035
# GRAPHQL SCHEMAS
10361036
# =============================================================================
1037-
1038-
10391037
class GraphQLRequest(BaseModel):
10401038
"""Request payload for GraphQL queries."""
10411039
query: str = Field(..., description="GraphQL query string", json_schema_extra={"examples": ["{ devices { devMac devName } }"]})

0 commit comments

Comments
 (0)