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

Feat/ngrok auth #373

Draft
wants to merge 6 commits into
base: development
Choose a base branch
from
Draft
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
3 changes: 3 additions & 0 deletions packages/server/src/server/databases/server/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ export const DEFAULT_DB_ITEMS: { [key: string]: () => any } = {
ngrok_key: () => "",
ngrok_protocol: () => "http",
ngrok_region: () => "us",
ngrok_user: () => "",
ngrok_password: () => "",
ngrok_auth_enabled: () => 0,
use_custom_certificate: () => 0,
password: () => "",
auto_caffeinate: () => 0,
Expand Down
5 changes: 5 additions & 0 deletions packages/server/src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,11 @@ class BlueBubblesServer extends EventEmitter {
await this.restartProxyServices();
}

// If the ngrok region is different, restart the ngrok process
if (prevConfig.ngrok_auth_enabled !== nextConfig.ngrok_auth_enabled && !proxiesRestarted) {
await this.restartProxyServices();
}

// Install the bundle if the Private API is turned on
if (!prevConfig.enable_private_api && nextConfig.enable_private_api) {
if (Server().privateApiHelper === null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ export class NgrokService extends Proxy {
async connect(): Promise<string> {
// If there is a ngrok API key set, and we have a refresh timer going, kill it
const ngrokKey = Server().repo.getConfig("ngrok_key") as string;

// Grab ngrok auth options
const ngrokUser = Server().repo.getConfig("ngrok_user") as string;
const ngrokPassword = Server().repo.getConfig("ngrok_password") as string;
const ngrokAuthEnabled = Server().repo.getConfig("ngrok_auth_enabled") as boolean;

let ngrokProtocol = (Server().repo.getConfig("ngrok_protocol") as Ngrok.Protocol) ?? "http";

const opts: Ngrok.Options = {
Expand Down Expand Up @@ -79,6 +85,13 @@ export class NgrokService extends Proxy {
ngrokProtocol = "http";
}

// If ngrok auth is enabled, and username and password are not empty, enable it
if (ngrokAuthEnabled) {
if (!isEmpty(ngrokUser) && !isEmpty(ngrokPassword)){
opts.auth = `${safeTrim(ngrokUser)}:${safeTrim(ngrokPassword)}`
}
}

// Set the protocol
opts.proto = ngrokProtocol;

Expand Down
160 changes: 160 additions & 0 deletions packages/ui/src/app/components/fields/NgrokAuthCredentialsFields.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import React, { useEffect, useState } from 'react';
import {
FormControl,
FormLabel,
FormHelperText,
Input,
IconButton,
FormErrorMessage,
useBoolean,
Text,
Button,
Stack,
HStack,
Box
} from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from '../../hooks';
import { showSuccessToast } from '../../utils/ToastUtils';
import { setConfig } from '../../slices/ConfigSlice';
import { AiFillEye, AiFillEyeInvisible, AiOutlineSave } from 'react-icons/ai';
import { baseTheme } from '../../../theme';
import { FaSleigh } from 'react-icons/fa';


export interface NgrokAuthCredentialsFieldsProps {
helpText?: string;
}

export const NgrokAuthCredentialsFields = ({ helpText }: NgrokAuthCredentialsFieldsProps): JSX.Element => {
const dispatch = useAppDispatch();
const ngrokUser: string = (useAppSelector(state => state.config.ngrok_user) ?? '');
const ngrokPassword: string = (useAppSelector(state => state.config.ngrok_password) ?? '');

const [showNgrokPassword, setShowNgrokPassword] = useBoolean();
const [newNgrokPassword, setNewNgrokPassword] = useState(ngrokPassword);
const [newNgrokUser, setNewNgrokUser] = useState(ngrokUser);

const [ngrokCredentialsError, setNgrokCredentialsError] = useState('');

useEffect(() => { setNewNgrokUser(ngrokUser); }, [ngrokUser]);
useEffect(() => { setNewNgrokPassword(ngrokPassword); }, [ngrokPassword]);


/**
* A handler and validator to enable Ngrok Tunnel Authentication
*
* @param theNewNgrokUser - The ngrok username
* @param theNewNgrokPassword - The ngrok password
*/
const enableNgrokBasicAuth = (theNewNgrokUser: string, theNewNgrokPassword: string): void => {
theNewNgrokUser = theNewNgrokUser.trim();
theNewNgrokPassword = theNewNgrokPassword.trim();
let errorMsg = '';
// Validate the user and pass
if (theNewNgrokUser.length < 4){
errorMsg = 'Username must be at least 4 characters long.';
}
if (theNewNgrokPassword.length < 8){
errorMsg = errorMsg + ' Password must be at least 8 characters long.';
}
// if errorMsg is not empty, user or pass is invalid so abort
if (errorMsg.length > 0) {
setNgrokCredentialsError(errorMsg);
return;
}

// if execution reaches this block, user and pass are valid so set config values properly
setNgrokCredentialsError('');
dispatch(setConfig({ name: 'ngrok_auth_enabled', value: true }));
dispatch(setConfig({ name: 'ngrok_user', value: theNewNgrokUser }));
dispatch(setConfig({ name: 'ngrok_password', value: theNewNgrokPassword }));
showSuccessToast({
id: 'settings',
duration: 4000,
description: 'Successfully ENABLED Ngrok Authentication! Restarting proxy services...'
});
};

const disableNgrokBasicAuth = () => {
setNewNgrokUser('');
setNewNgrokPassword('');
setNgrokCredentialsError('');
dispatch(setConfig({ name: 'ngrok_user', value: '' }));
dispatch(setConfig({ name: 'ngrok_password', value: '' }));
dispatch(setConfig({ name: 'ngrok_auth_enabled', value: false}));
showSuccessToast({
id: 'settings',
duration: 4000,
description: 'Successfully DISABLED Ngrok Authentication. Restarting proxy services...'
});
};

return (
<FormControl isInvalid={ngrokCredentialsError != '' ?? true}>
<FormLabel htmlFor='ngrok_user'>Ngrok Authentication (Very Optional)</FormLabel>
<HStack>
<Box>
<Button
onClick={() => disableNgrokBasicAuth()}
>
Disable
</Button>
</Box>
<Box>
<Input
id='ngrok_user'
placeholder='username'
type='text'
maxWidth="20em"
value={newNgrokUser}
onChange={(e) => {
if (ngrokCredentialsError == '') setNgrokCredentialsError('');
setNewNgrokUser(e.target.value);
}}
/>
</Box>
<Box>
<Input
id='ngrok_password'
placeholder='password'
type={showNgrokPassword ? 'text' : 'password'}
maxWidth="20em"
value={newNgrokPassword}
onChange={(e) => {
if (ngrokCredentialsError == '') setNgrokCredentialsError('');
setNewNgrokPassword(e.target.value);
}}
/>
</Box>
<Box>
<IconButton
ml={3}
verticalAlign='top'
aria-label='View Ngrok password'
icon={showNgrokPassword ? <AiFillEye /> : <AiFillEyeInvisible />}
onClick={() => setShowNgrokPassword.toggle()}
/>
<IconButton
ml={3}
verticalAlign='top'
aria-label='Save Ngrok password'
icon={<AiOutlineSave />}
onClick={() => enableNgrokBasicAuth(newNgrokUser, newNgrokPassword)}
/>
</Box>
</HStack>
{ ngrokCredentialsError == '' ? (
<FormHelperText>
{helpText ?? (
<Text>
This an optional additional security measure to protect your ngrok tunnel/server. This will require authentication
to the tunnel before a client is able to connect to your bluebubbles sever.
</Text>
)}
</FormHelperText>
) : (
<FormErrorMessage>{ngrokCredentialsError}</FormErrorMessage>
)}
</FormControl>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { AiOutlineInfoCircle } from 'react-icons/ai';
import { useAppSelector } from '../../../hooks';
import { NgrokRegionField } from '../../../components/fields/NgrokRegionField';
import { NgrokAuthTokenField } from '../../../components/fields/NgrokAuthTokenField';
import { NgrokAuthCredentialsFields } from '../../../components/fields/NgrokAuthCredentialsFields';
import { ProxyServiceField } from '../../../components/fields/ProxyServiceField';
import { ServerPasswordField } from '../../../components/fields/ServerPasswordField';
import { LocalPortField } from '../../../components/fields/LocalPortField';
Expand All @@ -32,6 +33,7 @@ import { EncryptCommunicationsField } from '../../../components/fields/EncryptCo

export const ConnectionSettings = (): JSX.Element => {
const proxyService: string = (useAppSelector(state => state.config.proxy_service) ?? '').toLowerCase().replace(' ', '-');
const ngrokToken: string = (useAppSelector(state => state.config.ngrok_key) ?? '').toLowerCase();

return (
<Stack direction='column' p={5}>
Expand Down Expand Up @@ -63,6 +65,8 @@ export const ConnectionSettings = (): JSX.Element => {
<Spacer />
{(proxyService === 'ngrok') ? (<NgrokAuthTokenField />) : null}
<Spacer />
{(proxyService === 'ngrok' && ngrokToken != '') ? (<NgrokAuthCredentialsFields />) : null}
<Spacer />
<Divider orientation='horizontal' />
<ServerPasswordField />
<LocalPortField />
Expand Down