Skip to content

Commit

Permalink
ADD: OTP Handler (#2015)
Browse files Browse the repository at this point in the history
* ADD: OTP Handler

* ADJUST: int tests

* Update:  update i18n in otp modal

* REFACTOR: format code

---------

Co-authored-by: mabasian <[email protected]>
  • Loading branch information
NeoPlays and mabasian authored Sep 10, 2024
1 parent 8fc751a commit 67520e2
Show file tree
Hide file tree
Showing 11 changed files with 198 additions and 15 deletions.
1 change: 1 addition & 0 deletions controls/roles/manage-service/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
dest: "{{ item.split(':') | first }}/prysm-{{ stereum_service_configuration.network }}-genesis.ssz"
mode: 0644
force: false
timeout: 200
become: yes
when: >
(stereum_service_configuration.service == 'PrysmBeaconService') and
Expand Down
77 changes: 77 additions & 0 deletions launcher/src/backend/AuthenticationService.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as QRCode from "qrcode";
import * as log from "electron-log";

export class AuthenticationService {
constructor(nodeConnection) {
Expand Down Expand Up @@ -168,4 +169,80 @@ export class AuthenticationService {
const url = await QRCode.toDataURL(otpauth);
return url;
}

static async handleOTPChange(oldPassword, newPassword, sshService) {
return new Promise((resolve, reject) => {
let oldPasswordWritten = false;
let newPasswordWrittenInitial = false;
let newPasswordWrittenConfirmation = false;
const conn = sshService.getConnectionFromPool();
conn.shell((err, stream) => {
// Set timeout for 20 seconds for the password change
setTimeout(() => {
stream.end();
conn.end();
reject("Timeout");
}, 20000);

// Catch enitial error
if (err) throw err;
stream.on("close", () => {
log.info("Closing OTP handle stream...");
resolve();
});

// Catch error
stream.on("error", (err) => {
stream.end();
conn.end();
reject(err);
});

// Handle data
stream.on("data", (data) => {
const recieved = data.toString().toLowerCase();

// Check if current password is being asked
if (new RegExp(/^(?=.*\b(current|old)\b)(?=.*\bpassword\b).*$/gm).test(recieved) && !oldPasswordWritten) {
oldPasswordWritten = true;
stream.write(`${oldPassword}\r\n`);

// Check if new password is being asked
} else if (
new RegExp(/^(?=.*\b(new)\b)(?=.*\bpassword\b).*$/gm).test(recieved) &&
oldPasswordWritten &&
!newPasswordWrittenInitial
) {
newPasswordWrittenInitial = true;
stream.write(`${newPassword}\r\n`);

// Check for password confirmation
} else if (
new RegExp(/^(?=.*\b(retype|repeat|confirm)\b)(?=.*\bpassword\b).*$/gm).test(recieved) &&
oldPasswordWritten &&
newPasswordWrittenInitial &&
!newPasswordWrittenConfirmation
) {
newPasswordWrittenConfirmation = true;
stream.write(`${newPassword}\r\n`);

// Check for Errors
} else if (new RegExp(/.*\b(error)\b.*/gm).test(recieved)) {
reject("Error changing password: " + recieved);

// Check for success
} else if (
recieved.includes(sshService.connectionInfo.user || "root") &&
oldPasswordWritten &&
newPasswordWrittenInitial &&
newPasswordWrittenConfirmation
) {
stream.end();
conn.end();
resolve();
}
});
});
});
}
}
14 changes: 11 additions & 3 deletions launcher/src/backend/NodeConnection.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,17 @@ export class NodeConnection {
}

async establish(taskManager, currentWindow) {
await this.sshService.connect(this.nodeConnectionParams, currentWindow);
await this.findStereumSettings();
this.taskManager = taskManager;
try {
if (this.sshService.connectionPool.length > 0) {
await this.sshService.disconnect(true);
}
await this.sshService.connect(this.nodeConnectionParams, currentWindow);
this.sshService.addingConnection = true;
await this.findStereumSettings();
this.taskManager = taskManager;
} catch (error) {
throw new Error(error);
}
}

/**
Expand Down
22 changes: 12 additions & 10 deletions launcher/src/backend/SSHService.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export class SSHService {

if (
this.connectionInfo &&
!this.addingConnection &&
this.addingConnection &&
(this.connectionPool.length < 6 || this.connectionPool[threshholdIndex]?._chanMgr?._count > 0)
) {
await this.connect(this.connectionInfo);
Expand Down Expand Up @@ -118,8 +118,8 @@ export class SSHService {

async connect(connectionInfo, currentWindow = null) {
this.connectionInfo = connectionInfo;
this.addingConnection = true;
let conn = new Client();
let passwordBanner = false;
return new Promise((resolve, reject) => {
conn.on("error", (error) => {
this.addingConnection = false;
Expand All @@ -130,6 +130,7 @@ export class SSHService {
//only works for ubuntu 22.04
conn.on("banner", (msg) => {
if (new RegExp(/^(?=.*\bchange\b)(?=.*\bpassword\b).*$/gm).test(msg.toLowerCase())) {
passwordBanner = true;
if (process.env.NODE_ENV === "test") {
resolve(conn);
}
Expand All @@ -148,17 +149,18 @@ export class SSHService {
.on("ready", async () => {
this.connectionPool.push(conn);
this.connected = true;
this.addingConnection = false;
if (this.connectionPool.length === 1) {
let test = await this.exec("ls");
if (new RegExp(/^(?=.*\bchange\b)(?=.*\bpassword\b).*$/gm).test(test.stderr.toLowerCase())) {
if (process.env.NODE_ENV === "test") {
resolve(conn);
if (!passwordBanner) {
if (this.connectionPool.length === 1) {
let test = await this.exec("ls");
if (new RegExp(/^(?=.*\bchange\b)(?=.*\bpassword\b).*$/gm).test(test.stderr.toLowerCase())) {
if (process.env.NODE_ENV === "test") {
resolve(conn);
}
reject(test.stderr);
}
reject(test.stderr);
}
resolve(conn);
}
resolve(conn);
})
.connect({
host: connectionInfo.host,
Expand Down
8 changes: 8 additions & 0 deletions launcher/src/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,14 @@ ipcMain.handle("readGasConfigFile", async (event, args) => {
return await tekuGasLimitConfig.readGasConfigFile(args);
});

ipcMain.handle("handleOTPChange", async (event, args) => {
return await AuthenticationService.handleOTPChange(
nodeConnection.nodeConnectionParams.password,
args.newPassword,
nodeConnection.sshService
);
});

ipcMain.handle("fetchObolCharonAlerts", async () => {
return await monitoring.fetchObolCharonAlerts();
});
Expand Down
21 changes: 21 additions & 0 deletions launcher/src/components/UI/server-management/MultiServerScreen.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import ServerHeader from './components/ServerHeader.vue';
<GenerateKey v-if="serverStore.isGenerateModalActive" @close-modal="closeGenerateModal" @generate-key="generateKeyHandler" />
<RemoveModal v-if="serverStore.isRemoveModalActive" @remove-handler="removeServerHandler" @close-window="closeWindow" />
<TwofactorModal v-if="isTwoFactorAuthActive" @submit-auth="submitAuthHandler" @close-window="closeAndCancel" />
<ChangeOTPModal v-if="serverStore.isOTPActive" @submit-password="submitPasswordHandler" @close-window="closeAndCancel" />
<ErrorModal v-if="serverStore.errorMsgExists" :description="serverStore.error" @close-window="closeErrorDialog" />
<QRcodeModal v-if="authStore.isBarcodeModalActive" @close-window="closeBarcode" />
</div>
Expand All @@ -32,6 +33,7 @@ import PasswordModal from "./components/modals/PasswordModal.vue";
import SwitchAnimation from "./components/SwitchAnimation.vue";
import TwofactorModal from "./components/modals/TwofactorModal.vue";
import GenerateKey from "./components/modals/GenerateKey.vue";
import ChangeOTPModal from "./components/modals/ChangeOTPModal.vue";
import { ref, onMounted, watchEffect, onUnmounted } from "vue";
import ControlService from "@/store/ControlService";
Expand Down Expand Up @@ -118,6 +120,24 @@ const submitAuthHandler = async (val) => {
loginHandler(val);
};
const submitPasswordHandler = async (pass) => {
serverStore.isOTPActive = false;
serverStore.isServerAnimationActive = true;
serverStore.connectingProcess = true;
loginAbortController = new AbortController();
try {
await ControlService.handleOTPChange({ newPassword: pass });
} catch (error) {
console.error("Couldn't Change Password:", error);
serverStore.isServerAnimationActive = false;
serverStore.errorMsgExists = true;
serverStore.error = "Couldn't Change Password. Please try again.\n" + error;
return;
}
serverStore.loginState.password = pass;
await loginHandler();
};
//Server Management Login Handler
const loginHandler = async (authCode) => {
Expand Down Expand Up @@ -220,6 +240,7 @@ const acceptChangePass = async (pass) => {
const closeWindow = () => {
serverStore.isRemoveModalActive = false;
isTwoFactorAuthActive.value = false;
serverStore.isOTPActive = false;
};
const closeErrorDialog = () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<template>
<custom-modal
icon="/img/icon/server-management-icons/form-error.png"
icon-size="w-24"
bg-color="bg-[#1c1d1d]"
:main-title="'Password Change Required'"
:confirm-text="t('twoFactorAuth.submit')"
:click-outside-text="t('twoFactorAuth.submit')"
:is-disabled="btnDisabled"
@close-window="closeWindow"
@confirm-action="submitPassword"
>
<template #content>
<div class="2fa-content-parent w-full h-full grid grid-cols-24 grid-rows-6 items-center">
<span class="col-start-5 col-end-21 row-start-1 row-end-3 text-md text-center text-gray-300">
{{ t("otpModal.newPass") }}
</span>
<input
v-model="password"
class="col-start-6 col-end-20 row-start-5 row-span-3 h-full rounded-lg px-2 text-md text-gray-800"
type="password"
/>
</div>
</template>
</custom-modal>
</template>

<script setup>
import CustomModal from "../../../node-page/components/modals/CustomModal.vue";
import { ref, computed } from "vue";
import i18n from "@/includes/i18n";
const t = i18n.global.t;
const password = ref("");
const emit = defineEmits(["submitPassword", "close-window"]);
const btnDisabled = computed(() => {
return !password.value;
});
const closeWindow = () => {
password.value = "";
emit("close-window");
};
const submitPassword = () => {
emit("submitPassword", password.value);
};
</script>

<style scoped></style>
8 changes: 6 additions & 2 deletions launcher/src/composables/useLogin.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ export const useServerLogin = () => {
}
startShell();
} catch (error) {
if (typeof error.message === "string" && error.message.toLowerCase().includes("password")) {
serverStore.isOTPActive = true;
serverStore.isServerAnimationActive = false;
serverStore.connectingProcess = false;
return;
}
console.error("Login failed:", error);
serverStore.isServerAnimationActive = false;
serverStore.errorMsgExists = true;
Expand All @@ -121,8 +127,6 @@ export const useServerLogin = () => {
serverStore.isServerAnimationActive = false;
serverStore.connectingProcess = false;
return;
} else if (typeof error === "string" && error.toLowerCase().includes("password")) {
serverStore.error = "You need to change your password first";
}

router.push("/login");
Expand Down
3 changes: 3 additions & 0 deletions launcher/src/languages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1055,5 +1055,8 @@
"rem": "click to remove 2FA",
"send": "click to send the verification code",
"zoomQr": "click to zoom in the QR code"
},
"otpModal": {
"newPass": "Type in your new Password"
}
}
4 changes: 4 additions & 0 deletions launcher/src/store/ControlService.js
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,10 @@ class ControlService extends EventEmitter {
return this.promiseIpc.send("readGasConfigFile", args);
}

async handleOTPChange(args) {
return this.promiseIpc.send("handleOTPChange", args);
}

async fetchObolCharonAlerts() {
return this.promiseIpc.send("fetchObolCharonAlerts");
}
Expand Down
3 changes: 3 additions & 0 deletions launcher/src/store/servers.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ export const useServers = defineStore("servers", {
{ name: "2fa", icon: "/img/icon/server-management-icons/2fa.png", isActive: false, isDisabled: false },
],
selectedTab: null,

//OTP Handling
isOTPActive: false,
};
},
actions: {
Expand Down

0 comments on commit 67520e2

Please sign in to comment.