diff --git a/docker/Dockerfile b/docker/Dockerfile index ca944504..b5153dc1 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -24,14 +24,23 @@ RUN apk add --no-cache python3 make g++ \ # Stage 4: Final production image FROM mongo:4.4-focal # Install Node.js and nginx, cleanup in the same layer -RUN apt-get update && apt-get install -y --no-install-recommends \ - nginx \ - curl \ - && curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \ - && apt-get install -y --no-install-recommends nodejs \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* \ - && rm -rf /var/cache/apt/* +RUN set -ex; \ + # Add MongoDB repository key + curl -fsSL https://pgp.mongodb.com/server-4.4.asc | gpg --dearmor -o /usr/share/keyrings/mongodb-archive-keyring.gpg; \ + echo "deb [signed-by=/usr/share/keyrings/mongodb-archive-keyring.gpg] http://repo.mongodb.org/apt/ubuntu focal/mongodb-org/4.4 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-4.4.list; \ + # Update and install packages + apt-get update && \ + apt-get install -y --no-install-recommends \ + nginx \ + curl \ + gnupg && \ + # Install Node.js + curl -fsSL https://deb.nodesource.com/setup_18.x | bash - && \ + apt-get install -y --no-install-recommends nodejs && \ + # Cleanup + apt-get clean && \ + rm -rf /var/lib/apt/lists/* && \ + rm -rf /var/cache/apt/* # Configure nginx and copy frontend COPY docker/nginx.conf /etc/nginx/nginx.conf diff --git a/src/backend/ssh.cjs b/src/backend/ssh.cjs index 0fd3ba16..af6b8c86 100644 --- a/src/backend/ssh.cjs +++ b/src/backend/ssh.cjs @@ -32,7 +32,7 @@ io.on("connection", (socket) => { return; } - if (!hostConfig.password && !hostConfig.rsaKey) { + if (!hostConfig.password && !hostConfig.privateKey) { logger.error("No authentication provided"); socket.emit("error", "Authentication required"); return; @@ -42,11 +42,12 @@ io.on("connection", (socket) => { ip: hostConfig.ip, port: hostConfig.port, user: hostConfig.user, - authType: hostConfig.password ? 'password' : 'public key', + authType: hostConfig.password ? 'password' : 'key', + keyType: hostConfig.keyType }; logger.info("Connecting with config:", safeHostConfig); - const { ip, port, user, password, rsaKey } = hostConfig; + const { ip, port, user, password, privateKey, passphrase } = hostConfig; const conn = new SSHClient(); conn @@ -98,7 +99,30 @@ io.on("connection", (socket) => { port: port, username: user, password: password, - privateKey: rsaKey ? Buffer.from(rsaKey) : undefined, + privateKey: privateKey ? Buffer.from(privateKey) : undefined, + passphrase: passphrase, + tryKeyboard: true, + algorithms: { + kex: [ + 'curve25519-sha256', + 'curve25519-sha256@libssh.org', + 'ecdh-sha2-nistp256', + 'ecdh-sha2-nistp384', + 'ecdh-sha2-nistp521', + 'diffie-hellman-group-exchange-sha256', + 'diffie-hellman-group14-sha256', + 'diffie-hellman-group14-sha1' + ], + serverHostKey: [ + 'ssh-ed25519', + 'ecdsa-sha2-nistp256', + 'ecdsa-sha2-nistp384', + 'ecdsa-sha2-nistp521', + 'rsa-sha2-512', + 'rsa-sha2-256', + 'ssh-rsa' + ] + } }); }); diff --git a/src/modals/AddHostModal.jsx b/src/modals/AddHostModal.jsx index 01880945..d0110386 100644 --- a/src/modals/AddHostModal.jsx +++ b/src/modals/AddHostModal.jsx @@ -26,20 +26,52 @@ import VisibilityOff from '@mui/icons-material/VisibilityOff'; const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidden }) => { const [showPassword, setShowPassword] = useState(false); + const [showPassphrase, setShowPassphrase] = useState(false); const [activeTab, setActiveTab] = useState(0); const handleFileChange = (e) => { const file = e.target.files[0]; - if (file) { - if (file.name.endsWith('.key') || file.name.endsWith('.pem') || file.name.endsWith('.pub')) { - const reader = new FileReader(); - reader.onload = (event) => { - setForm({ ...form, rsaKey: event.target.result }); - }; - reader.readAsText(file); - } else { - alert("Please upload a valid public key file."); - } + const supportedKeyTypes = { + 'id_rsa': 'RSA', + 'id_ed25519': 'ED25519', + 'id_ecdsa': 'ECDSA', + 'id_dsa': 'DSA', + '.pem': 'PEM', + '.key': 'KEY', + '.ppk': 'PPK' + }; + + const isValidKeyFile = Object.keys(supportedKeyTypes).some(ext => + file.name.toLowerCase().includes(ext) || file.name.endsWith('.pub') + ); + + if (isValidKeyFile) { + const reader = new FileReader(); + reader.onload = (event) => { + const keyContent = event.target.result; + let keyType = 'UNKNOWN'; + + // Detect key type from content + if (keyContent.includes('BEGIN RSA PRIVATE KEY') || keyContent.includes('BEGIN RSA PUBLIC KEY')) { + keyType = 'RSA'; + } else if (keyContent.includes('BEGIN OPENSSH PRIVATE KEY') && keyContent.includes('ssh-ed25519')) { + keyType = 'ED25519'; + } else if (keyContent.includes('BEGIN EC PRIVATE KEY') || keyContent.includes('BEGIN EC PUBLIC KEY')) { + keyType = 'ECDSA'; + } else if (keyContent.includes('BEGIN DSA PRIVATE KEY')) { + keyType = 'DSA'; + } + + setForm({ + ...form, + privateKey: keyContent, + keyType: keyType, + authMethod: 'key' + }); + }; + reader.readAsText(file); + } else { + alert('Please upload a valid SSH key file (RSA, ED25519, ECDSA, DSA, PEM, or PPK format).'); } }; @@ -48,7 +80,9 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd ...prev, authMethod: newMethod, password: "", - rsaKey: "" + privateKey: "", + keyType: "", + passphrase: "" })); }; @@ -59,7 +93,7 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd if (form.rememberHost) { if (form.authMethod === 'Select Auth') return false; - if (form.authMethod === 'rsaKey' && !form.rsaKey) return false; + if (form.authMethod === 'key' && !form.privateKey) return false; if (form.authMethod === 'password' && !form.password) return false; } @@ -81,7 +115,8 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd ip: '', user: '', password: '', - rsaKey: '', + privateKey: '', + keyType: '', port: 22, authMethod: 'Select Auth', rememberHost: false, @@ -241,7 +276,9 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd ...((!e.target.checked) && { authMethod: 'Select Auth', password: '', - rsaKey: '', + privateKey: '', + keyType: '', + passphrase: '', storePassword: true }) })} @@ -280,7 +317,7 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd > - + @@ -311,32 +348,49 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd )} - {form.authMethod === 'rsaKey' && ( - - Public Key - - + {form.authMethod === 'key' && ( + + + SSH Key + + + {form.privateKey && ( + + Key Passphrase (optional) + setForm(prev => ({ ...prev, passphrase: e.target.value }))} + endDecorator={ + setShowPassphrase(!showPassphrase)}> + {showPassphrase ? : } + + } + /> + + )} + )} )} @@ -380,7 +434,8 @@ AddHostModal.propTypes = { ip: PropTypes.string.isRequired, user: PropTypes.string.isRequired, password: PropTypes.string, - rsaKey: PropTypes.string, + privateKey: PropTypes.string, + keyType: PropTypes.string, port: PropTypes.number.isRequired, authMethod: PropTypes.string.isRequired, rememberHost: PropTypes.bool, diff --git a/src/modals/EditHostModal.jsx b/src/modals/EditHostModal.jsx index 048fb12d..623b0529 100644 --- a/src/modals/EditHostModal.jsx +++ b/src/modals/EditHostModal.jsx @@ -28,6 +28,7 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH const [showPassword, setShowPassword] = useState(false); const [activeTab, setActiveTab] = useState(0); const [isLoading, setIsLoading] = useState(false); + const [showPassphrase, setShowPassphrase] = useState(false); useEffect(() => { if (!isHidden && hostConfig) { @@ -48,14 +49,47 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH const handleFileChange = (e) => { const file = e.target.files[0]; - if (file.name.endsWith('.rsa') || file.name.endsWith('.key') || file.name.endsWith('.pem') || file.name.endsWith('.der') || file.name.endsWith('.p8') || file.name.endsWith('.ssh') || file.name.endsWith('.pub')) { + const supportedKeyTypes = { + 'id_rsa': 'RSA', + 'id_ed25519': 'ED25519', + 'id_ecdsa': 'ECDSA', + 'id_dsa': 'DSA', + '.pem': 'PEM', + '.key': 'KEY', + '.ppk': 'PPK' + }; + + const isValidKeyFile = Object.keys(supportedKeyTypes).some(ext => + file.name.toLowerCase().includes(ext) || file.name.endsWith('.pub') + ); + + if (isValidKeyFile) { const reader = new FileReader(); reader.onload = (evt) => { - setForm((prev) => ({ ...prev, rsaKey: evt.target.result })); + const keyContent = evt.target.result; + let keyType = 'UNKNOWN'; + + // Detect key type from content + if (keyContent.includes('BEGIN RSA PRIVATE KEY') || keyContent.includes('BEGIN RSA PUBLIC KEY')) { + keyType = 'RSA'; + } else if (keyContent.includes('BEGIN OPENSSH PRIVATE KEY') && keyContent.includes('ssh-ed25519')) { + keyType = 'ED25519'; + } else if (keyContent.includes('BEGIN EC PRIVATE KEY') || keyContent.includes('BEGIN EC PUBLIC KEY')) { + keyType = 'ECDSA'; + } else if (keyContent.includes('BEGIN DSA PRIVATE KEY')) { + keyType = 'DSA'; + } + + setForm((prev) => ({ + ...prev, + privateKey: keyContent, + keyType: keyType, + authMethod: 'key' + })); }; reader.readAsText(file); } else { - alert('Please upload a valid RSA private key file.'); + alert('Please upload a valid SSH key file (RSA, ED25519, ECDSA, DSA, PEM, or PPK format).'); } }; @@ -281,6 +315,7 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH + )} @@ -352,6 +387,64 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH )} )} + + {form.authMethod === 'key' && form.storePassword && ( + + + SSH Key + + {hostConfig?.privateKey && !form.privateKey && ( + + Existing {hostConfig.keyType || 'SSH'} key detected. Upload to replace. + + )} + + {form.privateKey && ( + + Key Passphrase (optional) + setForm(prev => ({ ...prev, passphrase: e.target.value }))} + endDecorator={ + setShowPassphrase(!showPassphrase)}> + {showPassphrase ? : } + + } + /> + + )} + + )} diff --git a/src/modals/NoAuthenticationModal.jsx b/src/modals/NoAuthenticationModal.jsx index 6829aaf0..813f1e02 100644 --- a/src/modals/NoAuthenticationModal.jsx +++ b/src/modals/NoAuthenticationModal.jsx @@ -19,30 +19,72 @@ import { useState, useEffect } from 'react'; import Visibility from '@mui/icons-material/Visibility'; import VisibilityOff from '@mui/icons-material/VisibilityOff'; -const NoAuthenticationModal = ({ isHidden, form, setForm, setIsNoAuthHidden, handleAuthSubmit }) => { +const NoAuthenticationModal = ({ isHidden, setIsHidden, onAuthenticate }) => { + const [form, setForm] = useState({ + authMethod: 'Select Auth', + password: '', + privateKey: '', + keyType: '', + passphrase: '' + }); const [showPassword, setShowPassword] = useState(false); + const [showPassphrase, setShowPassphrase] = useState(false); - useEffect(() => { - if (!form.authMethod) { - setForm(prev => ({ - ...prev, - authMethod: 'Select Auth' - })); - } - }, []); - - const isFormValid = () => { - if (!form.authMethod || form.authMethod === 'Select Auth') return false; - if (form.authMethod === 'rsaKey' && !form.rsaKey) return false; - if (form.authMethod === 'password' && !form.password) return false; - return true; + const handleSubmit = (e) => { + e.preventDefault(); + onAuthenticate({ + authMethod: form.authMethod, + password: form.password, + privateKey: form.privateKey, + keyType: form.keyType, + passphrase: form.passphrase + }); + setIsHidden(true); }; - const handleSubmit = (event) => { - event.preventDefault(); - if (isFormValid()) { - handleAuthSubmit(form); - setForm({ authMethod: 'Select Auth', password: '', rsaKey: '' }); + const handleFileChange = (e) => { + const file = e.target.files[0]; + const supportedKeyTypes = { + 'id_rsa': 'RSA', + 'id_ed25519': 'ED25519', + 'id_ecdsa': 'ECDSA', + 'id_dsa': 'DSA', + '.pem': 'PEM', + '.key': 'KEY', + '.ppk': 'PPK' + }; + + const isValidKeyFile = Object.keys(supportedKeyTypes).some(ext => + file.name.toLowerCase().includes(ext) || file.name.endsWith('.pub') + ); + + if (isValidKeyFile) { + const reader = new FileReader(); + reader.onload = (event) => { + const keyContent = event.target.result; + let keyType = 'UNKNOWN'; + + // Detect key type from content + if (keyContent.includes('BEGIN RSA PRIVATE KEY') || keyContent.includes('BEGIN RSA PUBLIC KEY')) { + keyType = 'RSA'; + } else if (keyContent.includes('BEGIN OPENSSH PRIVATE KEY') && keyContent.includes('ssh-ed25519')) { + keyType = 'ED25519'; + } else if (keyContent.includes('BEGIN EC PRIVATE KEY') || keyContent.includes('BEGIN EC PUBLIC KEY')) { + keyType = 'ECDSA'; + } else if (keyContent.includes('BEGIN DSA PRIVATE KEY')) { + keyType = 'DSA'; + } + + setForm({ + ...form, + privateKey: keyContent, + keyType: keyType, + authMethod: 'key' + }); + }; + reader.readAsText(file); + } else { + alert('Please upload a valid SSH key file (RSA, ED25519, ECDSA, DSA, PEM, or PPK format).'); } }; @@ -50,31 +92,12 @@ const NoAuthenticationModal = ({ isHidden, form, setForm, setIsNoAuthHidden, han { - if (reason !== 'backdropClick') { - setIsNoAuthHidden(true); - } - }} - sx={{ - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - }} + onClose={() => setIsHidden(true)} > Authentication Required @@ -84,8 +107,15 @@ const NoAuthenticationModal = ({ isHidden, form, setForm, setIsNoAuthHidden, han Authentication Method {form.authMethod === 'password' && ( Password -
- setForm(prev => ({ ...prev, password: e.target.value }))} - sx={{ - backgroundColor: theme.palette.general.primary, - color: theme.palette.text.primary, - flex: 1 - }} - /> - setShowPassword(!showPassword)} - sx={{ - color: theme.palette.text.primary, - marginLeft: 1 - }} - > - {showPassword ? : } - -
+ setForm({ ...form, password: e.target.value })} + endDecorator={ + setShowPassword(!showPassword)}> + {showPassword ? : } + + } + />
)} - {form.authMethod === 'rsaKey' && ( - - Public Key - - + > + {form.privateKey ? `Change ${form.keyType || 'SSH'} Key File` : 'Upload SSH Key File'} + + + + {form.privateKey && ( + + Key Passphrase (optional) + setForm(prev => ({ ...prev, passphrase: e.target.value }))} + endDecorator={ + setShowPassphrase(!showPassphrase)}> + {showPassphrase ? : } + + } + /> + + )} + )}