preliminary web support for auth (#26)

Some caveats:

  * it doesn't record the peer IP yet, which makes it harder to verify
    sessions are valid. This is a little annoying to do in hyper now
    (see hyperium/hyper#1410). The direct peer might not be what we want
    right now anyway because there's no TLS support yet (see #27).  In
    the meantime, the sane way to expose Moonfire NVR to the Internet is
    via a proxy server, and recording the proxy's IP is not useful.
    Maybe better to interpret a RFC 7239 Forwarded header (and/or
    the older X-Forwarded-{For,Proto} headers).

  * it doesn't ever use Secure (https-only) cookies, for a similar reason.
    It's not safe to use even with a tls proxy until this is fixed.

  * there's no "moonfire-nvr config" support for inspecting/invalidating
    sessions yet.

  * in debug builds, logging in is crazy slow. See libpasta/libpasta#9.

Some notes:

  * I removed the Javascript "no-use-before-defined" lint, as some of
    the functions form a cycle.

  * Fixed #20 along the way. I needed to add support for properly
    returning non-OK HTTP statuses to signal unauthorized and such.

  * I removed the Access-Control-Allow-Origin header support, which was
    at odds with the "SameSite=lax" in the cookie header. The "yarn
    start" method for running a local proxy server accomplishes the same
    thing as the Access-Control-Allow-Origin support in a more secure
    manner.
This commit is contained in:
Scott Lamb
2018-11-25 21:31:50 -08:00
parent 679370c77a
commit 422cd2a75e
21 changed files with 923 additions and 212 deletions

View File

@@ -65,6 +65,7 @@ import MoonfireAPI from './lib/MoonfireAPI';
const api = new MoonfireAPI();
let streamViews = null; // StreamView objects
let calendarView = null; // CalendarView object
let loginDialog = null;
/**
* Currently selected time format specification.
@@ -206,7 +207,34 @@ function fetch(selectedRange, videoLength) {
}
/**
* Initialize the page after receiving camera data.
* Updates the session bar at the top of the page.
*
* @param {Object} session the "session" key of the main API request's JSON,
* or null.
*/
function updateSession(session) {
let sessionBar = $('#session');
sessionBar.empty();
if (session === null) {
sessionBar.hide();
return;
}
sessionBar.append($('<span id="session-username" />').text(session.username));
let logout = $('<a>logout</a>');
logout.click(() => {
api
.logout(session.csrf)
.done(() => {
onReceivedTopLevel(null);
loginDialog.dialog('open');
});
});
sessionBar.append(' | ', logout);
sessionBar.show();
}
/**
* Initialize the page after receiving top-level data.
*
* Sets the following globals:
* zone - timezone from data received
@@ -215,9 +243,16 @@ function fetch(selectedRange, videoLength) {
* Builds the dom for the left side controllers
*
* @param {Object} data JSON resulting from the main API request /api/?days=
* or null if the request failed.
*/
function onReceivedCameras(data) {
newTimeZone(data.timeZoneName);
function onReceivedTopLevel(data) {
if (data === null) {
data = {cameras: [], session: null};
} else {
newTimeZone(data.timeZoneName);
}
updateSession(data.session);
// Set up controls and values
const nvrSettingsView = new NVRSettingsView();
@@ -236,6 +271,9 @@ function onReceivedCameras(data) {
const streamsParent = $('#streams');
const videos = $('#videos');
streamsParent.empty();
videos.empty();
streamViews = [];
let streamSelectorCameras = [];
for (const cameraJson of data.cameras) {
@@ -276,6 +314,57 @@ function onReceivedCameras(data) {
console.log('Loaded: ' + streamViews.length + ' stream views');
}
/**
* Handles the submit action on the login form.
*/
function sendLoginRequest() {
if (loginDialog.pending) {
return;
}
let username = $('#login-username').val();
let password = $('#login-password').val();
let submit = $('#login-submit');
let error = $('#login-error');
error.empty();
error.removeClass('ui-state-highlight');
submit.button('option', 'disabled', true);
loginDialog.pending = true;
console.info('logging in as', username);
api
.login(username, password)
.done(() => {
console.info('login successful');
loginDialog.dialog('close');
sendTopLevelRequest();
})
.catch((e) => {
console.info('login failed:', e);
error.show();
error.addClass('ui-state-highlight');
error.text(e.responseText);
})
.always(() => {
submit.button('option', 'disabled', false);
loginDialog.pending = false;
});
}
/** Sends the top-level api request. */
function sendTopLevelRequest() {
api
.request(api.nvrUrl(true))
.done((data) => onReceivedTopLevel(data))
.catch((e) => {
console.error('NVR load exception: ', e);
onReceivedTopLevel(null);
if (e.status == 401) {
loginDialog.dialog('open');
}
});
}
/**
* Class representing the entire application.
*/
@@ -289,16 +378,22 @@ export default class NVRApplication {
* Start the application.
*/
start() {
api
.request(api.nvrUrl(true))
.done((data) => onReceivedCameras(data))
.fail((req, status, err) => {
console.error('NVR load error: ', status, err);
onReceivedCameras({cameras: []});
})
.catch((e) => {
console.error('NVR load exception: ', e);
onReceivedCameras({cameras: []});
});
loginDialog = $('#login').dialog({
autoOpen: false,
modal: true,
buttons: [
{
id: 'login-submit',
text: 'Login',
click: sendLoginRequest,
},
],
});
loginDialog.pending = false;
loginDialog.find('form').on('submit', function(event) {
event.preventDefault();
sendLoginRequest();
});
sendTopLevelRequest();
}
}

View File

@@ -4,6 +4,9 @@ body {
#nav {
float: left;
}
#session {
float: right;
}
#datetime .ui-datepicker {
width: 100%;

View File

@@ -5,6 +5,8 @@
<title>Moonfire NVR</title>
</head>
<body>
<div id="session">
</div>
<div id="nav">
<form action="#">
<fieldset>
@@ -72,6 +74,27 @@
</fieldset>
</form>
</div>
<table id="videos"></table>
</body>
<table id="videos"></table>
<div id="login">
<form>
<fieldset>
<table>
<tr>
<td><label for="login-username">Username:</label></td>
<td><input type="text" id="login-username" name="username"></td>
</tr>
<tr>
<td><label for="login-password">Password:</label></td>
<td><input type="password" id="login-password" name="password"></td>
</tr>
<tr>
<td></td>
<td><input type="submit" tabindex="-1" style="position:absolute; top:-1000px"></td>
</tr>
</table>
<p id="login-error"></p>
</fieldset>
</form>
</div>
</body>
</html>

View File

@@ -158,4 +158,37 @@ export default class MoonfireAPI {
cache: cacheOk,
});
}
/**
* Start a new AJAX request to log in.
*
* @param {String} username
* @param {String} password
* @return {Request}
*/
login(username, password) {
return $.ajax(this._builder.makeUrl('login'), {
data: {
username: username,
password: password,
},
method: 'POST',
});
}
/**
* Start a new AJAX request to log out.
*
* @param {String} csrf: the csrf request token as returned in
* <tt>/api/</tt> response JSON.
* @return {Request}
*/
logout(csrf) {
return $.ajax(this._builder.makeUrl('logout'), {
data: {
csrf: csrf,
},
method: 'POST',
});
}
}