diff --git a/lib/utils/config.js b/lib/utils/config.js index d1795a7b2..51def335c 100644 --- a/lib/utils/config.js +++ b/lib/utils/config.js @@ -1,42 +1,43 @@ // @flow import invariant from 'invariant'; import type { CallSingleKeyserverEndpoint } from './call-single-keyserver-endpoint.js'; import type { DispatchRecoveryAttempt, CallKeyserverEndpoint, } from '../keyserver-conn/keyserver-conn-types.js'; import type { InitialNotifMessageOptions } from '../shared/crypto-utils.js'; import type { LogInActionSource } from '../types/account-types.js'; import type { PlatformDetails } from '../types/device-types.js'; export type Config = { +resolveKeyserverSessionInvalidationUsingNativeCredentials: ?( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, callKeyserverEndpoint: CallKeyserverEndpoint, dispatchRecoveryAttempt: DispatchRecoveryAttempt, logInActionSource: LogInActionSource, keyserverID: string, getInitialNotificationsEncryptedMessage?: ( keyserverID: string, options?: ?InitialNotifMessageOptions, ) => Promise, ) => Promise, +setSessionIDOnRequest: boolean, +calendarRangeInactivityLimit: ?number, +platformDetails: PlatformDetails, + +authoritativeKeyserverID: string, }; let registeredConfig: ?Config = null; const registerConfig = (config: Config) => { registeredConfig = { ...registeredConfig, ...config }; }; const getConfig = (): Config => { invariant(registeredConfig, 'config should be set'); return registeredConfig; }; export { registerConfig, getConfig }; diff --git a/native/authoritative-keyserver.js b/native/authoritative-keyserver.js new file mode 100644 index 000000000..84686b8b8 --- /dev/null +++ b/native/authoritative-keyserver.js @@ -0,0 +1,19 @@ +// @flow + +import { ashoatKeyserverID } from 'lib/utils/validation-utils.js'; + +function getAuthoritativeKeyserverID(): string { + try { + const { + authoritativeKeyserverID: keyserverID, + // $FlowExpectedError: file might not exist + } = require('./facts/authoritative_keyserver.json'); + return keyserverID; + } catch { + return ashoatKeyserverID; + } +} + +const authoritativeKeyserverID: string = getAuthoritativeKeyserverID(); + +export { authoritativeKeyserverID }; diff --git a/native/config.js b/native/config.js index 1c7d6d8b0..750cac0da 100644 --- a/native/config.js +++ b/native/config.js @@ -1,19 +1,21 @@ // @flow import { Platform } from 'react-native'; import { registerConfig } from 'lib/utils/config.js'; import { resolveKeyserverSessionInvalidationUsingNativeCredentials } from './account/legacy-recover-keyserver-session.js'; +import { authoritativeKeyserverID } from './authoritative-keyserver.js'; import { persistConfig, codeVersion } from './redux/persist.js'; registerConfig({ resolveKeyserverSessionInvalidationUsingNativeCredentials, setSessionIDOnRequest: false, calendarRangeInactivityLimit: 15 * 60 * 1000, platformDetails: { platform: Platform.OS, codeVersion, stateVersion: persistConfig.version, }, + authoritativeKeyserverID, }); diff --git a/web/app.react.js b/web/app.react.js index 5fd940151..5db1f5cdb 100644 --- a/web/app.react.js +++ b/web/app.react.js @@ -1,430 +1,432 @@ // @flow import 'basscss/css/basscss.min.css'; import './theme.css'; import { config as faConfig } from '@fortawesome/fontawesome-svg-core'; import classnames from 'classnames'; import _isEqual from 'lodash/fp/isEqual.js'; import * as React from 'react'; import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; import { WagmiConfig } from 'wagmi'; import { fetchEntriesActionTypes, updateCalendarQueryActionTypes, } from 'lib/actions/entry-actions.js'; import { ChatMentionContextProvider } from 'lib/components/chat-mention-provider.react.js'; import { EditUserAvatarProvider } from 'lib/components/edit-user-avatar-provider.react.js'; import { ModalProvider, useModalContext, } from 'lib/components/modal-provider.react.js'; import { StaffContextProvider } from 'lib/components/staff-provider.react.js'; import { createLoadingStatusSelector, combineLoadingStatuses, } from 'lib/selectors/loading-selectors.js'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; import { extractMajorDesktopVersion } from 'lib/shared/version-utils.js'; import { TunnelbrokerProvider } from 'lib/tunnelbroker/tunnelbroker-context.js'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import type { WebNavInfo } from 'lib/types/nav-types.js'; import type { Dispatch } from 'lib/types/redux-types.js'; import { getConfig, registerConfig } from 'lib/utils/config.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { infoFromURL } from 'lib/utils/url-utils.js'; import { AlchemyENSCacheProvider, wagmiConfig } from 'lib/utils/wagmi-utils.js'; import QrCodeLogin from './account/qr-code-login.react.js'; import AppThemeWrapper from './app-theme-wrapper.react.js'; +import { authoritativeKeyserverID } from './authoritative-keyserver.js'; import WebEditThreadAvatarProvider from './avatars/web-edit-thread-avatar-provider.react.js'; import Calendar from './calendar/calendar.react.js'; import Chat from './chat/chat.react.js'; import { EditModalProvider } from './chat/edit-message-provider.js'; import { MemberListSidebarProvider } from './chat/member-list-sidebar/member-list-sidebar-provider.react.js'; import NavigationArrows from './components/navigation-arrows.react.js'; import { initOpaque } from './crypto/opaque-utils.js'; import { getDatabaseModule } from './database/database-module-provider.js'; import electron from './electron.js'; import InputStateContainer from './input/input-state-container.react.js'; import InviteLinkHandler from './invite-links/invite-link-handler.react.js'; import InviteLinksRefresher from './invite-links/invite-links-refresher.react.js'; import LoadingIndicator from './loading-indicator.react.js'; import { MenuProvider } from './menu-provider.react.js'; import UpdateModalHandler from './modals/update-modal.react.js'; import SettingsSwitcher from './navigation-panels/settings-switcher.react.js'; import Topbar from './navigation-panels/topbar.react.js'; import useBadgeHandler from './push-notif/badge-handler.react.js'; import { PushNotificationsHandler } from './push-notif/push-notifs-handler.js'; import { updateNavInfoActionType } from './redux/action-types.js'; import DisconnectedBar from './redux/disconnected-bar.js'; import FocusHandler from './redux/focus-handler.react.js'; import { KeyserverReachabilityHandler } from './redux/keyserver-reachability-handler.js'; import { persistConfig } from './redux/persist.js'; import PolicyAcknowledgmentHandler from './redux/policy-acknowledgment-handler.js'; import { useSelector } from './redux/redux-utils.js'; import VisibilityHandler from './redux/visibility-handler.react.js'; import history from './router-history.js'; import { MessageSearchStateProvider } from './search/message-search-state-provider.react.js'; import { createTunnelbrokerInitMessage } from './selectors/tunnelbroker-selectors.js'; import AccountSettings from './settings/account-settings.react.js'; import DangerZone from './settings/danger-zone.react.js'; import KeyserverSelectionList from './settings/keyserver-selection-list.react.js'; import CommunityPicker from './sidebar/community-picker.react.js'; import Splash from './splash/splash.react.js'; import './typography.css'; import css from './style.css'; import { TooltipProvider } from './tooltips/tooltip-provider.js'; import { canonicalURLFromReduxState, navInfoFromURL } from './url-utils.js'; void initOpaque(); // We want Webpack's css-loader and style-loader to handle the Fontawesome CSS, // so we disable the autoAddCss logic and import the CSS file. Otherwise every // icon flashes huge for a second before the CSS is loaded. import '@fortawesome/fontawesome-svg-core/styles.css'; faConfig.autoAddCss = false; const desktopDetails = electron?.version ? { majorDesktopVersion: extractMajorDesktopVersion(electron?.version) } : null; registerConfig({ // We can't securely cache credentials on web resolveKeyserverSessionInvalidationUsingNativeCredentials: null, setSessionIDOnRequest: true, // Never reset the calendar range calendarRangeInactivityLimit: null, platformDetails: { platform: electron?.platform ?? 'web', codeVersion: 71, stateVersion: persistConfig.version, ...desktopDetails, }, + authoritativeKeyserverID, }); const versionBroadcast = new BroadcastChannel('comm_version'); versionBroadcast.postMessage(getConfig().platformDetails.codeVersion); versionBroadcast.onmessage = (event: MessageEvent) => { if (event.data && event.data !== getConfig().platformDetails.codeVersion) { location.reload(); } }; // Start initializing the database immediately void getDatabaseModule(); type BaseProps = { +location: { +pathname: string, ... }, }; type Props = { ...BaseProps, // Redux state +navInfo: WebNavInfo, +entriesLoadingStatus: LoadingStatus, +loggedIn: boolean, +activeThreadCurrentlyUnread: boolean, // Redux dispatch functions +dispatch: Dispatch, +modals: $ReadOnlyArray, }; class App extends React.PureComponent { componentDidMount() { const { navInfo, location: { pathname }, loggedIn, } = this.props; const newURL = canonicalURLFromReduxState(navInfo, pathname, loggedIn); if (pathname !== newURL) { history.replace(newURL); } } componentDidUpdate(prevProps: Props) { const { navInfo, location: { pathname }, loggedIn, } = this.props; if (!_isEqual(navInfo)(prevProps.navInfo)) { const newURL = canonicalURLFromReduxState(navInfo, pathname, loggedIn); if (newURL !== pathname) { history.push(newURL); } } else if (pathname !== prevProps.location.pathname) { const urlInfo = infoFromURL(pathname); const newNavInfo = navInfoFromURL(urlInfo, { navInfo }); if (!_isEqual(newNavInfo)(navInfo)) { this.props.dispatch({ type: updateNavInfoActionType, payload: newNavInfo, }); } } else if (loggedIn !== prevProps.loggedIn) { const newURL = canonicalURLFromReduxState(navInfo, pathname, loggedIn); if (newURL !== pathname) { history.replace(newURL); } } if (loggedIn !== prevProps.loggedIn) { electron?.clearHistory(); } } onWordmarkClicked = () => { this.props.dispatch({ type: updateNavInfoActionType, payload: { tab: 'chat' }, }); }; render(): React.Node { let content; if (this.props.loggedIn) { content = ( <> {this.renderMainContent()} {this.props.modals} ); } else { content = ( <> {this.renderLoginPage()} {this.props.modals} ); } return ( {content} ); } onHeaderDoubleClick = (): void => electron?.doubleClickTopBar(); stopDoubleClickPropagation: ?(SyntheticEvent) => void = electron ? e => e.stopPropagation() : null; renderLoginPage(): React.Node { const { loginMethod } = this.props.navInfo; if (loginMethod === 'qr-code') { return ; } return ; } renderMainContent(): React.Node { const mainContent = this.getMainContentWithSwitcher(); let navigationArrows = null; if (electron) { navigationArrows = ; } const headerClasses = classnames({ [css.header]: true, [css['electron-draggable']]: electron, }); const wordmarkClasses = classnames({ [css.wordmark]: true, [css['electron-non-draggable']]: electron, [css['wordmark-macos']]: electron?.platform === 'macos', }); return (

Comm

{navigationArrows}
{mainContent}
); } getMainContentWithSwitcher(): React.Node { const { tab, settingsSection } = this.props.navInfo; let mainContent: React.Node; if (tab === 'settings') { if (settingsSection === 'account') { mainContent = ; } else if (settingsSection === 'friend-list') { mainContent = null; } else if (settingsSection === 'block-list') { mainContent = null; } else if (settingsSection === 'keyservers') { mainContent = ; } else if (settingsSection === 'build-info') { mainContent = null; } else if (settingsSection === 'danger-zone') { mainContent = ; } return (
{mainContent}
); } if (tab === 'calendar') { mainContent = ; } else if (tab === 'chat') { mainContent = ; } const mainContentClass = classnames( css['main-content-container'], css['main-content-container-column'], ); return (
{mainContent}
); } } const fetchEntriesLoadingStatusSelector = createLoadingStatusSelector( fetchEntriesActionTypes, ); const updateCalendarQueryLoadingStatusSelector = createLoadingStatusSelector( updateCalendarQueryActionTypes, ); const ConnectedApp: React.ComponentType = React.memo( function ConnectedApp(props) { const activeChatThreadID = useSelector( state => state.navInfo.activeChatThreadID, ); const navInfo = useSelector(state => state.navInfo); const fetchEntriesLoadingStatus = useSelector( fetchEntriesLoadingStatusSelector, ); const updateCalendarQueryLoadingStatus = useSelector( updateCalendarQueryLoadingStatusSelector, ); const entriesLoadingStatus = combineLoadingStatuses( fetchEntriesLoadingStatus, updateCalendarQueryLoadingStatus, ); const loggedIn = useSelector(isLoggedIn); const activeThreadCurrentlyUnread = useSelector( state => !activeChatThreadID || !!state.threadStore.threadInfos[activeChatThreadID]?.currentUser.unread, ); useBadgeHandler(); const dispatch = useDispatch(); const modalContext = useModalContext(); const modals = React.useMemo( () => modalContext.modals.map(([modal, key]) => ( {modal} )), [modalContext.modals], ); const tunnelbrokerInitMessage = useSelector(createTunnelbrokerInitMessage); return ( ); }, ); function AppWithProvider(props: BaseProps): React.Node { return ( ); } export default AppWithProvider; diff --git a/web/authoritative-keyserver.js b/web/authoritative-keyserver.js new file mode 100644 index 000000000..397e75562 --- /dev/null +++ b/web/authoritative-keyserver.js @@ -0,0 +1,8 @@ +// @flow + +import { ashoatKeyserverID } from 'lib/utils/validation-utils.js'; + +const authoritativeKeyserverID: string = + process.env.AUTHORITATIVE_KEYSERVER_ID ?? ashoatKeyserverID; + +export { authoritativeKeyserverID }; diff --git a/web/webpack.config.cjs b/web/webpack.config.cjs index c4f842bcf..82c345e6b 100644 --- a/web/webpack.config.cjs +++ b/web/webpack.config.cjs @@ -1,225 +1,236 @@ const CopyPlugin = require('copy-webpack-plugin'); const path = require('path'); const { WebpackManifestPlugin } = require('webpack-manifest-plugin'); const { createProdBrowserConfig, createDevBrowserConfig, createNodeServerRenderingConfig, createWebWorkersConfig, } = require('lib/webpack/shared.cjs'); const babelConfig = require('./.babelrc.cjs'); async function getConfig(configName) { const { getCommConfig } = await import( - '../keyserver/dist/lib/utils/comm-config.js' + 'keyserver/dist/lib/utils/comm-config.js' ); return await getCommConfig(configName); } const baseBrowserConfig = { entry: { browser: ['./script.js'], }, output: { filename: 'prod.[contenthash:12].build.js', path: path.join(__dirname, 'dist'), }, resolve: { alias: { '../images': path.resolve('../keyserver/images'), }, fallback: { crypto: false, fs: false, path: false, }, }, }; const baseDevBrowserConfig = { ...baseBrowserConfig, output: { ...baseBrowserConfig.output, filename: 'dev.build.js', pathinfo: true, publicPath: 'http://localhost:8080/', }, devServer: { port: 8080, headers: { 'Access-Control-Allow-Origin': '*' }, allowedHosts: ['all'], host: '0.0.0.0', static: { directory: path.join(__dirname, 'dist'), }, }, plugins: [ new CopyPlugin({ patterns: [ { from: 'node_modules/@commapp/olm/olm.wasm', to: path.join(__dirname, 'dist'), }, ], }), new CopyPlugin({ patterns: [ { from: 'node_modules/@commapp/opaque-ke-wasm' + '/pkg/comm_opaque2_wasm_bg.wasm', to: path.join(__dirname, 'dist', 'opaque-ke.wasm'), }, ], }), ], }; const baseProdBrowserConfig = { ...baseBrowserConfig, plugins: [ new CopyPlugin({ patterns: [ { from: 'node_modules/@commapp/olm/olm.wasm', to: path.join(__dirname, 'dist', 'olm.[contenthash:12].wasm'), }, ], }), new CopyPlugin({ patterns: [ { from: 'node_modules/@commapp/opaque-ke-wasm' + '/pkg/comm_opaque2_wasm_bg.wasm', to: path.join(__dirname, 'dist', 'opaque-ke.[contenthash:12].wasm'), }, ], }), new WebpackManifestPlugin({ publicPath: '', }), ], }; const baseNodeServerRenderingConfig = { externals: ['react', 'react-dom', 'react-redux'], entry: { keyserver: ['./loading.react.js'], }, output: { filename: 'app.build.cjs', library: 'app', libraryTarget: 'commonjs2', path: path.join(__dirname, 'dist'), }, }; const baseWebWorkersConfig = { entry: { pushNotif: './push-notif/service-worker.js', database: './database/worker/db-worker.js', }, output: { filename: '[name].build.js', path: path.join(__dirname, 'dist', 'webworkers'), }, resolve: { fallback: { crypto: false, fs: false, path: false, }, }, }; const devWebWorkersPlugins = [ new CopyPlugin({ patterns: [ { from: 'database/_generated/comm_query_executor.wasm', to: path.join(__dirname, 'dist', 'webworkers'), }, ], }), new CopyPlugin({ patterns: [ { from: 'node_modules/@commapp/olm/olm.wasm', to: path.join(__dirname, 'dist', 'webworkers'), }, ], }), ]; const prodWebWorkersPlugins = [ new CopyPlugin({ patterns: [ { from: 'database/_generated/comm_query_executor.wasm', to: path.join( __dirname, 'dist', 'webworkers', 'comm_query_executor.[contenthash:12].wasm', ), }, ], }), new CopyPlugin({ patterns: [ { from: 'node_modules/@commapp/olm/olm.wasm', to: path.join( __dirname, 'dist', 'webworkers', 'olm.[contenthash:12].wasm', ), }, ], }), new WebpackManifestPlugin({ publicPath: '', }), ]; module.exports = async function (env) { const identityServiceConfig = await getConfig({ folder: 'secrets', name: 'identity_service_config', }); const identitySocketAddr = JSON.stringify( identityServiceConfig?.identitySocketAddr, ); - const envVars = { IDENTITY_SOCKET_ADDR: identitySocketAddr }; + const authoritativeKeyserverIDConfig = await getConfig({ + folder: 'facts', + name: 'authoritative_keyserver', + }); + const authoritativeKeyserverID = JSON.stringify( + authoritativeKeyserverIDConfig?.authoritativeKeyserverID, + ); + + const envVars = { + IDENTITY_SOCKET_ADDR: identitySocketAddr, + AUTHORITATIVE_KEYSERVER_ID: authoritativeKeyserverID, + }; const browserConfigPromise = env.prod ? createProdBrowserConfig(baseProdBrowserConfig, babelConfig, envVars) : createDevBrowserConfig(baseDevBrowserConfig, babelConfig, envVars); const nodeConfigPromise = createNodeServerRenderingConfig( baseNodeServerRenderingConfig, babelConfig, ); const [browserConfig, nodeConfig] = await Promise.all([ browserConfigPromise, nodeConfigPromise, ]); const nodeServerRenderingConfig = { ...nodeConfig, mode: env.prod ? 'production' : 'development', }; const workersConfig = { ...baseWebWorkersConfig, plugins: env.prod ? prodWebWorkersPlugins : devWebWorkersPlugins, }; const webWorkersConfig = createWebWorkersConfig( env, workersConfig, babelConfig, ); return [browserConfig, nodeServerRenderingConfig, webWorkersConfig]; };