From a4273d2d0aa76270da09eddc37742e129d9fee49 Mon Sep 17 00:00:00 2001 From: Arksine <9563098+Arksine@users.noreply.github.com> Date: Thu, 15 Apr 2021 20:33:42 -0400 Subject: [PATCH] test: implement JWT Authorization for test client Signed-off-by: Eric Callahan --- test/client/css/login_dialog.css | 128 ++++++++ test/client/dist/jquery.leanModal.min.js | 5 + test/client/img/close-button.png | Bin 0 -> 643 bytes test/client/index.html | 91 +++++- test/client/js/main.js | 364 +++++++++++++++++++++-- 5 files changed, 554 insertions(+), 34 deletions(-) create mode 100644 test/client/css/login_dialog.css create mode 100644 test/client/dist/jquery.leanModal.min.js create mode 100644 test/client/img/close-button.png diff --git a/test/client/css/login_dialog.css b/test/client/css/login_dialog.css new file mode 100644 index 0000000..b0f28b6 --- /dev/null +++ b/test/client/css/login_dialog.css @@ -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; +} diff --git a/test/client/dist/jquery.leanModal.min.js b/test/client/dist/jquery.leanModal.min.js new file mode 100644 index 0000000..a5772dd --- /dev/null +++ b/test/client/dist/jquery.leanModal.min.js @@ -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=$("
");$("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); diff --git a/test/client/img/close-button.png b/test/client/img/close-button.png new file mode 100644 index 0000000000000000000000000000000000000000..ae4caec847828bf27197b05c484e2e691e38371c GIT binary patch literal 643 zcmV-}0(||6P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0vJg|K~y+TwUp0G z6;T+5uhAw*2--$OZro*IXf4u>8fg@?4Jn}nS8dx}`UBDgLEK5Sl(ayjjfqN8Q9`?f zA`({y)hZ&Y=Q(q}%e@WPmA&xHJM*10-+brCoXTFSC3}Oa)vECxxC-5H1X|$}JcT9t zEZ`-T64J2)8;9N$Xs!*oLjMoNW1)tGG+Na>9D+Zv0!#1$tgsD+pbyk9ILCX>i^sZz z1W#bkz+~^>CNUG0MBGt4gk$g(x`@5UEBm};O@PU4=xvrv(dKYZ!bk9(`;e3>q{C&* zFFMQ~o>yUv`}1&z``5VrPGi$B$^9(2U@=^aScwXUvp*uZn(uGGMOY@*2_|!ePr)tl z1CC}jDQq5DiZCrNz$CDsMM!K9Jw8f{PT z&{lT))DrrV`k6iC9DgrvllFs~yIw!C+g3}sojrzbh0c|gL}e-spSI58F{wTKrk6+< zbLXmt?m;AT(PH?_7_CDG==5y=|1(Z7eoSu8fH@}TfV z|Lp@{f)Vgzz8l-9c9bNBH91| literal 0 HcmV?d00001 diff --git a/test/client/index.html b/test/client/index.html index cf66650..fd753dd 100644 --- a/test/client/index.html +++ b/test/client/index.html @@ -2,9 +2,11 @@ + - + +

Klippy Web API Test

@@ -67,10 +69,97 @@ Progress: +

+ + + + + +


+ + + +
+
+
+

Moonraker Login

+
+ +
+
+ + +
+
+ + +
+
+ +
+
+
+
+ + +
+
+
+

Create User

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
+ + +
+
+
+

Change Password

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
+ diff --git a/test/client/js/main.js b/test/client/js/main.js index f328581..872f3ec 100644 --- a/test/client/js/main.js +++ b/test/client/js/main.js @@ -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(); };