test: implement JWT Authorization for test client

Signed-off-by:  Eric Callahan <arksine.code@gmail.com>
This commit is contained in:
Arksine 2021-04-15 20:33:42 -04:00 committed by Arksine
parent 0c765f7b71
commit a4273d2d0a
5 changed files with 554 additions and 34 deletions

View File

@ -0,0 +1,128 @@
#lean_overlay {
position: fixed;
z-index:100;
top: 0px;
left: 0px;
height:100%;
width:100%;
background: #000;
display: none;
}
.user {
width: 404px;
padding-bottom: 2px;
display:none;
background: #FFF;
border-radius: 5px;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
box-shadow: 0px 0px 4px rgba(0,0,0,0.7);
-webkit-box-shadow: 0 0 4px rgba(0,0,0,0.7);
-moz-box-shadow: 0 0px 4px rgba(0,0,0,0.7);
}
.user-header {
/*background: url(../img/hd-bg.png);*/
background: #cecece;
padding: 8px 8px 8px 8px;
border-bottom: 1px solid #CCC;
border-top-left-radius: 5px;
-moz-border-radius-topleft: 5px;
-webkit-border-top-left-radius: 5px;
border-top-right-radius: 5px;
-moz-border-radius-topright: 5px;
-webkit-border-top-right-radius: 5px;
}
.user-header h2 {
color: #444;
font-size: 1.2em;
font-weight: 700;
margin-bottom: 3px;
text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.5);
}
.user-header p {
color: #444;
font-size: 1em;
margin: 0;
text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.5);
text-shadow: none;
}
.user .text-field {
position: relative;
width: 364px;
padding: 14px 20px;
border-bottom: 1px solid #EEE;
text-align: right;
}
.user .button-field {
width: 254px;
overflow: hidden;
padding: 12px 20px 12px 130px;
}
.user .text-field label {
display: block;
float: left;
width: 90px;
padding-top: 8px;
color: #222;
font-size: 1em;
text-align: left;
}
.user .text-field input {
width: 244px;
padding: 8px;
border-radius: 4px;
-moz-border-radius: 4px;
-webkit-border-radius: 4px;
font-size: 1em; color: #222;
background: #F7F7F7;
font-family: "Helvetica Neue";
outline: none;
border-top: 1px solid #CCC;
border-left: 1px solid #CCC;
border-right: 1px solid #E7E6E6;
border-bottom: 1px solid #E7E6E6;
}
/*.user .text-field input.good_input {
background: #DEF5E1 url(../img/good.png) 236px center no-repeat;
}*/
.user .text-field input.error_input {
background: #FDE0E0;
}
.user-button {
float: right;
font-family: "Helvetica Neue", "Helvetica", "Arial", sans-serif;
background: #cecece;
border: none;
width: auto;
overflow: visible;
font-size: 1.1em;
color: rgb(0, 0, 0);
padding: 7px 10px;
border-radius: 4px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
text-shadow: 0 1px 0 rgba(0,0,0,0.4);
}
.modal_close {
position: absolute;
top: 12px;
right: 12px;
display: inline-block;
border: none;
width: 20px;
height: 20px;
background: url(../img/close-button.png);
z-index: 2;
}

View File

@ -0,0 +1,5 @@
// leanModal v1.1 by Ray Stone - http://finelysliced.com.au
// Dual licensed under the MIT and GPL
(function($){$.fn.extend({leanModal:function(options){var defaults={top:100,overlay:0.5,closeButton:null};var overlay=$("<div id='lean_overlay'></div>");$("body").append(overlay);options=$.extend(defaults,options);return this.each(function(){var o=options;$(this).click(function(e){var modal_id=$(this).attr("href");$("#lean_overlay").click(function(){close_modal(modal_id)});$(o.closeButton).click(function(){close_modal(modal_id)});var modal_height=$(modal_id).outerHeight();var modal_width=$(modal_id).outerWidth();
$("#lean_overlay").css({"display":"block",opacity:0});$("#lean_overlay").fadeTo(200,o.overlay);$(modal_id).css({"display":"block","position":"fixed","opacity":0,"z-index":11000,"left":50+"%","margin-left":-(modal_width/2)+"px","top":o.top+"px"});$(modal_id).fadeTo(200,1);e.preventDefault()})});function close_modal(modal_id){$("#lean_overlay").fadeOut(200);$(modal_id).css({"display":"none"})}}})})(jQuery);

Binary file not shown.

After

Width:  |  Height:  |  Size: 643 B

View File

@ -2,9 +2,11 @@
<html>
<head>
<link rel="stylesheet" href="dist/themes/default/style.min.css"/>
<link rel="stylesheet" href="css/login_dialog.css"/>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script src="dist/jstree.min.js"></script>
<script src="/js/main.js?v=0.1.2" type="module"></script>
<script src="dist/jquery.leanModal.min.js"></script>
<script src="/js/main.js?v=0.1.3" type="module"></script>
</head>
<body>
<h3>Klippy Web API Test</h3>
@ -67,10 +69,97 @@ Progress: <progress id="progressbar" value="0" max="100"></progress>
<button id="btnshutdown" style="width: 9em">Shutdown OS</button>
<button id="btngetlog" style="width: 9em">Klippy Log</button>
<button id="btnmoonlog" style="width: 9em">Moonraker Log</button>
<br/><br/>
<button id="btnloginuser" style="width: 9em">Login User</button>
<button id="btncreateuser" style="width: 9em">Create User</button>
<button id="btnsetapikey" style="width: 9em">Set API Key</button>
<button id="btnlogout" style="width: 9em" class="req-login">Log Out</button>
<button id="btndeluser" style="width: 9em" class="req-login">Delete User</button>
<button id="btnchangepass" style="width: 9em" class="req-login">Change Pass</button>
<br/><br/>
<span id="filename" hidden></span></br>
<div id="streamdiv">
</div>
<a id="nav_home" href="#" hidden>Hidden Home</a>
<a id="do_login" rel="leanModal" name="login" href="#login" hidden>Hidden Login</a>
<div id="login" class="user">
<div id="login-ct">
<div id="login-header" class="user-header">
<h2>Moonraker Login</h2>
<button id="login_close" class="modal_close"></a>
</div>
<form id="login_form" class="user-form">
<div class="text-field">
<label for="login_username">Username</label>
<input id="login_username" class="good_input" name="login_username" type="text" />
</div>
<div class="text-field">
<label for="login_password">Password</label>
<input id="login_password" name="login_password" type="password" />
</div>
<div class="button-field">
<button type="submit" class="user-button">Login</button>
</div>
</form>
</div>
</div>
<a id="do_signup" rel="leanModal" name="signup" href="#signup" hidden>Hidden Signup</a>
<div id="signup" class="user">
<div id="signup-ct">
<div id="signup-header" class="user-header">
<h2>Create User</h2>
<button id="signup_close" class="modal_close"></a>
</div>
<form id="signup_form" class="user-form">
<div class="text-field">
<label for="signup_username">Username</label>
<input id="signup_username" class="good_input" name="signup_username" type="text" />
</div>
<div class="text-field">
<label for="signup_password">Password</label>
<input id="signup_password" name="signup_password" type="password" />
</div>
<div class="text-field">
<label for="signup_verify_pass">Re-Enter Password</label>
<input id="signup_verify_pass" name="signup_verify_pass" type="password" />
</div>
<div class="button-field">
<button type="submit" class="user-button">Signup</button>
</div>
</form>
</div>
</div>
<a id="do_changepass" rel="leanModal" name="changepass" href="#changepass" hidden></a>
<div id="changepass" class="user">
<div id="changepass-ct">
<div id="changepass-header" class="user-header">
<h2>Change Password</h2>
<button id="changepass_close" class="modal_close"></a>
</div>
<form id="changepass_form" class="user-form">
<div class="text-field">
<label for="changepass_oldpass">Current Password</label>
<input id="changepass_oldpass" name="changepass_oldpass" type="password" />
</div>
<div class="text-field">
<label for="changepass_newpass">New Password</label>
<input id="changepass_newpass" name="changepass_newpass" type="password" />
</div>
<div class="text-field">
<label for="changepass_verify_pass">Re-Enter New Password</label>
<input id="changepass_verify_pass" name="changepass_verify_pass" type="password" />
</div>
<div class="button-field">
<button type="submit" class="user-button">Submit</button>
</div>
</form>
</div>
</div>
</body>
</html>

View File

@ -120,6 +120,12 @@ var api = {
url: "/server/files/config_examples/"
},
// Server APIs
server_info: {
url: "/server/info",
method: "server.info"
},
// Machine APIs
reboot: {
url: "/machine/reboot",
@ -136,11 +142,28 @@ var api = {
},
oneshot_token: {
url: "/access/oneshot_token"
},
login: {
url: "/access/login"
},
logout: {
url: "/access/logout"
},
refresh_jwt: {
url: "/access/refresh_jwt"
},
user: {
url: "/access/user"
},
reset_password: {
url: "/access/user/password"
}
}
var websocket = null;
var apikey = null;
var auth_token = null;
var refresh_token = window.localStorage.getItem('refresh_token');
var paused = false;
var klippy_ready = false;
var api_type = 'http';
@ -859,6 +882,17 @@ function run_request(url, method, callback=null)
let settings = {
url: url,
method: method,
statusCode: {
401: function() {
if (refresh_token != null) {
refresh_json_web_token(run_request, url, method, callback);
}
else {
auth_token = null;
$("#do_login").click();
}
}
},
success: (resp, status) => {
console.log(resp);
if (callback != null)
@ -872,7 +906,9 @@ function run_request(url, method, callback=null)
settings.contentType = false,
settings.processData = false
}
if (apikey != null)
if (auth_token != null)
settings.headers = {"Authorization": `Bearer ${auth_token}`};
else if (apikey != null)
settings.headers = {"X-Api-Key": apikey};
$.ajax(settings);
}
@ -894,9 +930,9 @@ function form_delete_request(api_url, query_string="", callback=null) {
function form_download_request(uri) {
let dl_url = origin + uri;
if (apikey != null) {
form_get_request(api.oneshot_token.url,
callback=(resp) => {
if (apikey != null || auth_token != null) {
form_get_request(api.oneshot_token.url, "",
(resp) => {
let token = resp.result;
dl_url += "?token=" + token;
do_download(dl_url);
@ -961,7 +997,9 @@ function jstree_populate_children(node, callback) {
if (api_type == "http") {
let qs = `?path=${node.id}`;
let settings = {url: origin + api.directory.url + qs};
if (apikey != null)
if (auth_token != null)
settings.headers = {"Authorization": `Bearer ${auth_token}`};
else if (apikey != null)
settings.headers = {"X-Api-Key": apikey};
$.get(settings, (resp, status) => {
callback(generate_children(resp.result, node));
@ -1130,6 +1168,7 @@ class KlippyWebsocket {
this.onmessage = null;
this.onopen = null;
this.id = null;
this.reconnect = true
this.connect();
}
@ -1138,14 +1177,15 @@ class KlippyWebsocket {
// to reconnect if its closed. This is nice as it allows the
// client to easily recover from Klippy restarts without user
// intervention
if (apikey != null) {
if (apikey != null || auth_token != null) {
// Fetch a oneshot token to pass websocket authorization
let token_settings = {
url: origin + api.oneshot_token.url,
headers: {
"X-Api-Key": apikey
}
}
if (auth_token != null)
token_settings.headers = {"Authorization": `Bearer ${auth_token}`};
else
token_settings.headers = {"X-Api-Key": apikey};
$.get(token_settings, (data, status) => {
let token = data.result;
let url = this.base_address + "/websocket?token=" + token;
@ -1172,10 +1212,14 @@ class KlippyWebsocket {
klippy_ready = false;
this.connected = false;
this.id = null;
console.log("Websocket Closed, reconnecting in 1s: ", e.reason);
setTimeout(() => {
this.connect();
}, 1000);
// TODO: Need to cancel any pending JSON-RPC requests
if (this.reconnect) {
console.log("Websocket Closed, reconnecting in 1s: ", e.reason);
setTimeout(() => {
if (this.reconnect)
this.connect();
}, 1000);
}
};
this.ws.onerror = (err) => {
@ -1202,7 +1246,7 @@ class KlippyWebsocket {
}
close() {
// TODO: Cancel the timeout
this.reconnect = false
this.ws.close();
}
@ -1227,25 +1271,172 @@ function create_websocket(url) {
json_rpc.register_transport(websocket);
}
function login_jwt_user(user, pass, do_create) {
if (!user || !pass) {
alert("Invalid username/password")
return;
}
let close_btn_name = "#login_close"
if (do_create)
close_btn_name = "#signup_close"
let settings = {
url: origin + api.login.url,
data: JSON.stringify({username: user, password: pass}),
contentType: "application/json",
dataType: 'json'
}
if (do_create) {
settings.url = origin + api.user.url;
if (auth_token != null)
settings.headers = {"Authorization": `Bearer ${auth_token}`};
else if (apikey != null)
settings.headers = {"X-Api-Key": apikey};
}
$.post(settings, (resp, status) => {
let res = resp.result;
console.log("Login Response:");
console.log(res);
auth_token = res.token;
refresh_token = res.refresh_token;
window.localStorage.setItem('refresh_token', refresh_token);
$('.req-login').prop('disabled', false);
$(close_btn_name).click();
check_authorization();
}).fail(() => {
console.log("Login Failed");
alert("Login Failed")
});
}
function logout_jwt_user() {
if (auth_token == null) {
console.log("No User Logged In")
return;
}
let settings = {
url: origin + api.logout.url,
contentType: "application/json",
dataType: 'json',
headers: {
"Authorization": `Bearer ${auth_token}`
}
}
$.post(settings, (resp, status) => {
let res = resp.result;
console.log("Logout Response:");
console.log(res);
auth_token = null;
refresh_token = null;
window.localStorage.removeItem('refresh_token');
$('.req-login').prop('disabled', true);
}).fail(() => {
console.log("Logout User Failed");
});
}
function delete_jwt_user(pass) {
if (!pass) {
alert("Invalid Password, Cannot Delete User");
return;
}
if (auth_token == null) {
console.log("No User Logged In")
return;
}
let settings = {
method: 'DELETE',
url: origin + api.user.url,
contentType: "application/json",
data: JSON.stringify({password: pass}),
dataType: 'json',
headers: {
"Authorization": `Bearer ${auth_token}`
},
success: (resp, status) => {
let res = resp.result;
console.log("Delete User Response:");
console.log(res);
auth_token = null;
refresh_token = null;
window.localStorage.removeItem('refresh_token');
$('.req-login').prop('disabled', true);
}
}
$.ajax(settings)
.fail(() => {
console.log("Delete User Failed");
});
}
function change_jwt_password(old_pass, new_pass) {
if (!old_pass || !new_pass) {
alert("Invalid input for change password")
return;
}
let settings = {
url: origin + api.reset_password.url,
data: JSON.stringify({password: old_pass, new_password: new_pass}),
contentType: "application/json",
dataType: 'json',
headers: {
"Authorization": `Bearer ${auth_token}`
}
}
$.post(settings, (resp, status) => {
let res = resp.result;
console.log("Change Password Response:");
console.log(res);
$("#changepass_close").click();
}).fail(() => {
console.log("Failed to change password");
alert("Password Reset Failed")
});
}
function refresh_json_web_token(callback, ...args) {
let settings = {
url: origin + api.refresh_jwt.url,
data: JSON.stringify({refresh_token: refresh_token}),
contentType: "application/json",
dataType: 'json',
}
$.post(settings, (resp, status) => {
let res = resp.result;
console.log("Refresh JWT Response:");
console.log(res);
auth_token = res.token;
$('.req-login').prop('disabled', false);
if (callback != null)
callback(...args);
}).fail(() => {
console.log("Refresh JWT Failed");
auth_token = null;
refresh_token = null;
window.localStorage.removeItem('refresh_token');
$('.req-login').prop('disabled', true);
$("#do_login").click();
});
}
function check_authorization() {
// send a HTTP "run gcode" command
let settings = {
url: origin + api.printer_info.url,
url: origin + api.server_info.url,
statusCode: {
401: function() {
// Show APIKey Popup
let result = window.prompt("Enter a valid API Key:", "");
if (result == null || result.length != 32) {
console.log("Invalid API Key: " + result);
apikey = null;
} else {
apikey = result;
}
check_authorization();
if (refresh_token != null) {
refresh_json_web_token(check_authorization);
} else {
$("#do_login").click();
}
}
}
}
if (apikey != null)
if (auth_token != null)
settings.headers = {"Authorization": `Bearer ${auth_token}`};
else if (apikey != null)
settings.headers = {"X-Api-Key": apikey};
$.get(settings, (data, status) => {
// Create a websocket if /printer/info successfully returns
@ -1261,11 +1452,9 @@ function do_download(url) {
window.onload = () => {
// Handle changes between the HTTP and Websocket API
$('.reqws').prop('disabled', true);
$('.req-login').prop('disabled', true);
$('input[type=radio][name=test_type]').on('change', function() {
api_type = $(this).val();
let disable_transfer = (!$('#cbxFileTransfer').is(":checked") && is_printing);
$('.toggleable').prop(
'disabled', (api_type == 'websocket' || disable_transfer));
$('.reqws').prop('disabled', (api_type == 'http'));
$('#apimethod').prop('hidden', (api_type == "websocket"));
$('#apiargs').prop('hidden', (api_type == "http"));
@ -1467,7 +1656,9 @@ window.onload = () => {
let sendtype = $("input[type=radio][name=api_cmd_type]:checked").val();
let url = $('#apirequest').val();
let settings = {url: url}
if (apikey != null)
if (auth_token != null)
settings.headers = {"Authorization": `Bearer ${auth_token}`};
else if (apikey != null)
settings.headers = {"X-Api-Key": apikey};
if (sendtype == "get") {
console.log("Sending GET " + url);
@ -1553,7 +1744,9 @@ window.onload = () => {
return false;
}
};
if (apikey != null)
if (auth_token != null)
settings.headers = {"Authorization": `Bearer ${auth_token}`};
else if (apikey != null)
settings.headers = {"X-Api-Key": apikey};
$.ajax(settings);
$('#upload-file').val('');
@ -1645,11 +1838,13 @@ window.onload = () => {
$('#btntestmesh').click(() => {
if (api_type == 'http') {
let settings = {url: origin + api.object_status.url + "?bed_mesh"};
if (apikey != null)
if (auth_token != null)
settings.headers = {"Authorization": `Bearer ${auth_token}`};
else if (apikey != null)
settings.headers = {"X-Api-Key": apikey};
$.get(settings, (resp, status) => {
process_mesh(resp.result)
return false;
process_mesh(resp.result)
return false;
});
} else {
get_mesh();
@ -1726,5 +1921,108 @@ window.onload = () => {
form_download_request(api.moonraker_log.url);
});
$('#btnloginuser').click(() => {
$("#do_login").click();
});
$('#btncreateuser').click(() => {
$("#do_signup").click();
});
$('#btnlogout').click(() => {
logout_jwt_user();
});
$('#btndeluser').click(() => {
let password = window.prompt("Verify your password:")
delete_jwt_user(password);
});
$('#btnchangepass').click(() => {
$("#do_changepass").click();
});
$('#btnsetapikey').click(() => {
let defkey = apikey;
if (!defkey)
defkey = ""
let new_key = window.prompt("Enter your API Key", defkey);
if (!new_key)
apikey = null;
else
apikey = new_key;
check_authorization();
});
$("#do_login").leanModal({
top : 200,
overlay : 0.4,
closeButton: "#login_close"
});
$("#do_signup").leanModal({
top : 200,
overlay : 0.4,
closeButton: "#signup_close"
});
$("#do_changepass").leanModal({
top : 200,
overlay : 0.4,
closeButton: "#changepass_close"
});
$("#login_close").click(() => {
//$("#login_username").val("");
$("#login_password").val("");
$("#nav_home").click();
});
$("#signup_close").click(() => {
$("#signup_username").val("");
$("#signup_password").val("");
$("#signup_verify_pass").val("");
$("#nav_home").click();
});
$("#changepass_close").click(() => {
$("#changepass_oldpass").val("");
$("#changepass_newpass").val("");
$("#changepass_verify_pass").val("");
$("#nav_home").click();
});
$("#login_form").submit((evt)=> {
let user = $("#login_username").val()
let pass = $("#login_password").val()
if (user != "" && pass != "")
login_jwt_user(user, pass, false);
else
alert("Invalid username/password");
return false;
});
$("#signup_form").submit((evt)=> {
let user = $("#signup_username").val()
let pass = $("#signup_password").val()
let verify_pass = $("#signup_verify_pass").val()
if (user != "" && pass != "" && pass == verify_pass)
login_jwt_user(user, pass, true);
else
alert("Invalid username/password");
return false;
});
$("#changepass_form").submit((evt)=> {
let old_pass = $("#changepass_oldpass").val()
let new_pass = $("#changepass_newpass").val()
let verify_pass = $("#changepass_verify_pass").val()
if (old_pass != "" && new_pass != "" && new_pass == verify_pass)
change_jwt_password(old_pass, new_pass)
else
alert("All fields are required to change password");
return false;
});
check_authorization();
};