Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ADD: OTP Handler #2015

Merged
merged 6 commits into from
Sep 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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