Enforce Content-Security-Policy (CSP) in Obsidian, Desktop, Web apps
Prevent XSS attacks by enforcing Content-Security-Policy (CSP) in apps.
Do not allow loading images, other assets from untrusted domains.
- Only allow loading assets from trusted domains
like 'self', khoj.dev, ipapi for geolocation, google (fonts, img)
- images from khoj domain, google (for profile pic)
- assets from khoj domain
- Do not allow iframe src
- Allow unsafe-inline script and styles for now as markdown-it escapes html
in user, khoj chat
- Add hostURL to CSP of the Desktop, Obsidian apps
Given web client is served by khoj server, it doesn't need to
explicitly allow for khoj.dev domain. So if user self-hosting, it'll
automatically allow the domain in the CSP (via 'self')
Whereas the Obsidian, Desktop clients allow configure the server URL.
Note *switching server URL breaks CSP until app is reloaded*
2024-02-20 21:42:00 +00:00
const { app , BrowserWindow , ipcMain , Tray , Menu , nativeImage , shell , session } = require ( 'electron' ) ;
2023-09-06 19:04:18 +00:00
const todesktop = require ( "@todesktop/runtime" ) ;
2023-11-04 03:56:27 +00:00
const khojPackage = require ( './package.json' ) ;
2023-09-06 19:04:18 +00:00
todesktop . init ( ) ;
const fs = require ( 'fs' ) ;
const { dialog } = require ( 'electron' ) ;
const cron = require ( 'cron' ) . CronJob ;
const axios = require ( 'axios' ) ;
2023-11-09 21:34:27 +00:00
const KHOJ _URL = 'https://app.khoj.dev' ;
2023-09-06 19:04:18 +00:00
const Store = require ( 'electron-store' ) ;
2024-04-12 05:19:58 +00:00
const textFileTypes = [
// Default valid file extensions supported by Khoj
'org' , 'md' , 'markdown' , 'txt' , 'html' , 'xml' ,
// Other valid text file extensions from https://google.github.io/magika/model/config.json
'appleplist' , 'asm' , 'asp' , 'batch' , 'c' , 'cs' , 'css' , 'csv' , 'eml' , 'go' , 'html' , 'ini' , 'internetshortcut' , 'java' , 'javascript' , 'json' , 'latex' , 'lisp' , 'makefile' , 'markdown' , 'mht' , 'mum' , 'pem' , 'perl' , 'php' , 'powershell' , 'python' , 'rdf' , 'rst' , 'rtf' , 'ruby' , 'rust' , 'scala' , 'shell' , 'smali' , 'sql' , 'svg' , 'symlinktext' , 'txt' , 'vba' , 'winregistry' , 'xml' , 'yaml' ]
2024-07-01 13:00:00 +00:00
const binaryFileTypes = [ 'pdf' , 'jpg' , 'jpeg' , 'png' ]
2024-04-12 05:19:58 +00:00
const validFileTypes = textFileTypes . concat ( binaryFileTypes ) ;
2023-09-06 19:04:18 +00:00
const schema = {
files : {
type : 'array' ,
items : {
type : 'object' ,
properties : {
path : {
type : 'string'
}
}
} ,
default : [ ]
} ,
folders : {
type : 'array' ,
items : {
type : 'object' ,
properties : {
path : {
type : 'string'
}
}
} ,
default : [ ]
} ,
2023-10-26 19:33:03 +00:00
khojToken : {
type : 'string' ,
default : ''
} ,
2023-09-06 19:04:18 +00:00
hostURL : {
type : 'string' ,
default : KHOJ _URL
} ,
lastSync : {
type : 'array' ,
items : {
type : 'object' ,
properties : {
path : {
type : 'string'
} ,
datetime : {
type : 'string'
}
}
}
}
} ;
2023-11-09 21:34:27 +00:00
let syncing = false ;
2023-12-29 04:50:48 +00:00
let state = { }
2023-10-12 01:12:12 +00:00
const store = new Store ( { schema } ) ;
2023-09-06 19:04:18 +00:00
console . log ( store ) ;
// include the Node.js 'path' module at the top of your file
const path = require ( 'path' ) ;
function handleSetTitle ( event , title ) {
const webContents = event . sender
const win = BrowserWindow . fromWebContents ( webContents )
win . setTitle ( title )
dialog . showOpenDialog ( { properties : [ 'openFile' , 'openDirectory' ] } ) . then ( function ( response ) {
if ( ! response . canceled ) {
// handle fully qualified file name
console . log ( response . filePaths [ 0 ] ) ;
} else {
console . log ( "no file selected" ) ;
}
} ) ;
}
2023-10-12 01:12:12 +00:00
function filenameToMimeType ( filename ) {
const extension = filename . split ( '.' ) . pop ( ) ;
switch ( extension ) {
case 'pdf' :
return 'application/pdf' ;
case 'png' :
return 'image/png' ;
case 'jpg' :
case 'jpeg' :
return 'image/jpeg' ;
2023-10-17 09:37:20 +00:00
case 'md' :
2023-10-12 01:12:12 +00:00
case 'markdown' :
return 'text/markdown' ;
case 'org' :
return 'text/org' ;
default :
2024-02-24 14:29:03 +00:00
console . warn ( ` Unknown file type: ${ extension } . Defaulting to text/plain. ` ) ;
2023-10-12 01:12:12 +00:00
return 'text/plain' ;
}
}
2024-04-12 05:19:58 +00:00
function isSupportedFileType ( filePath ) {
2024-04-23 05:44:57 +00:00
const fileExtension = filePath . split ( '.' ) . pop ( ) . toLowerCase ( ) ;
2024-04-12 05:19:58 +00:00
return validFileTypes . includes ( fileExtension ) ;
2024-04-02 20:19:15 +00:00
}
2024-04-12 06:19:25 +00:00
function processDirectory ( filesToPush , folder ) {
2024-04-23 05:31:20 +00:00
try {
const files = fs . readdirSync ( folder . path , { withFileTypes : true } ) ;
for ( const file of files ) {
const filePath = path . join ( file . path , file . name || '' ) ;
// Skip hidden files and folders
if ( file . name . startsWith ( '.' ) ) {
continue ;
}
// Add supported files to index
if ( file . isFile ( ) && isSupportedFileType ( filePath ) ) {
console . log ( ` Add ${ file . name } in ${ file . path } for indexing ` ) ;
filesToPush . push ( filePath ) ;
}
// Recursively process subdirectories
if ( file . isDirectory ( ) ) {
processDirectory ( filesToPush , { 'path' : filePath } ) ;
}
2023-12-03 19:03:29 +00:00
}
2024-04-23 05:31:20 +00:00
} catch ( err ) {
if ( err . code === 'EACCES' ) {
console . error ( ` Access denied to ${ folder . path } ` ) ;
} else if ( err . code === 'ENOENT' ) {
console . error ( ` ${ folder . path } does not exist ` ) ;
} else {
console . error ( ` An error occurred while reading directory: ${ error . message } ` ) ;
2023-12-03 19:03:29 +00:00
}
2024-04-23 05:31:20 +00:00
return ;
2023-12-03 19:03:29 +00:00
}
2024-04-23 05:31:20 +00:00
2023-12-03 19:03:29 +00:00
}
2024-04-12 06:19:25 +00:00
function pushDataToKhoj ( regenerate = false ) {
2023-11-07 11:17:42 +00:00
// Don't sync if token or hostURL is not set or if already syncing
2023-11-09 01:59:02 +00:00
if ( store . get ( 'khojToken' ) === '' || store . get ( 'hostURL' ) === '' || syncing === true ) {
2023-11-07 11:17:42 +00:00
const win = BrowserWindow . getAllWindows ( ) [ 0 ] ;
if ( win ) win . webContents . send ( 'update-state' , state ) ;
return ;
} else {
2023-11-09 01:59:02 +00:00
syncing = true ;
2023-11-07 11:17:42 +00:00
}
2023-09-06 19:04:18 +00:00
let filesToPush = [ ] ;
2023-10-12 01:12:12 +00:00
const files = store . get ( 'files' ) || [ ] ;
const folders = store . get ( 'folders' ) || [ ] ;
state = { completed : true }
2023-09-06 19:04:18 +00:00
2023-10-18 08:00:41 +00:00
// Collect paths of all configured files to index
2023-10-12 01:12:12 +00:00
for ( const file of files ) {
2024-04-25 09:49:59 +00:00
// Remove files that no longer exist
if ( ! fs . existsSync ( file . path ) ) {
console . error ( ` ${ file . path } does not exist ` ) ;
continue ;
}
2023-10-12 01:12:12 +00:00
filesToPush . push ( file . path ) ;
2023-09-06 19:04:18 +00:00
}
2023-10-12 01:12:12 +00:00
2023-10-18 08:00:41 +00:00
// Collect paths of all indexable files in configured folders
2023-10-12 01:12:12 +00:00
for ( const folder of folders ) {
2024-04-25 09:49:59 +00:00
// Remove folders that no longer exist
if ( ! fs . existsSync ( folder . path ) ) {
console . error ( ` ${ folder . path } does not exist ` ) ;
continue ;
}
2024-04-12 06:19:25 +00:00
processDirectory ( filesToPush , folder ) ;
2023-09-06 19:04:18 +00:00
}
const lastSync = store . get ( 'lastSync' ) || [ ] ;
2024-01-03 17:38:20 +00:00
const filesDataToPush = [ ] ;
2023-10-12 01:12:12 +00:00
for ( const file of filesToPush ) {
2023-09-06 19:04:18 +00:00
const stats = fs . statSync ( file ) ;
2023-09-12 23:35:07 +00:00
if ( ! regenerate ) {
2023-10-18 08:00:41 +00:00
// Only push files that have been modified since last sync
2023-09-12 23:35:07 +00:00
if ( stats . mtime . toISOString ( ) < lastSync . find ( ( syncedFile ) => syncedFile . path === file ) ? . datetime ) {
continue ;
}
2023-09-06 19:04:18 +00:00
}
2023-09-12 23:35:07 +00:00
2023-10-18 08:00:41 +00:00
// Collect all updated or newly created files since last sync to index on Khoj server
2023-09-06 19:04:18 +00:00
try {
2023-10-17 20:05:50 +00:00
let encoding = binaryFileTypes . includes ( file . split ( '.' ) . pop ( ) ) ? "binary" : "utf8" ;
let mimeType = filenameToMimeType ( file ) + ( encoding === "utf8" ? "; charset=UTF-8" : "" ) ;
let fileContent = Buffer . from ( fs . readFileSync ( file , { encoding : encoding } ) , encoding ) ;
let fileObj = new Blob ( [ fileContent ] , { type : mimeType } ) ;
2024-01-03 17:38:20 +00:00
filesDataToPush . push ( { blob : fileObj , path : file } ) ;
2023-09-06 19:04:18 +00:00
state [ file ] = {
success : true ,
}
} catch ( err ) {
console . error ( err ) ;
state [ file ] = {
success : false ,
error : err
}
}
}
2023-10-18 08:00:41 +00:00
// Mark deleted files for removal from index on Khoj server
2023-09-06 19:04:18 +00:00
for ( const syncedFile of lastSync ) {
if ( ! filesToPush . includes ( syncedFile . path ) ) {
2023-10-12 01:12:12 +00:00
fileObj = new Blob ( [ "" ] , { type : filenameToMimeType ( syncedFile . path ) } ) ;
2024-01-03 17:38:20 +00:00
filesDataToPush . push ( { blob : fileObj , path : syncedFile . path } ) ;
2023-09-06 19:04:18 +00:00
}
}
2023-10-18 08:00:41 +00:00
// Send collected files to Khoj server for indexing
2024-01-03 17:38:20 +00:00
const hostURL = store . get ( 'hostURL' ) || KHOJ _URL ;
const headers = { 'Authorization' : ` Bearer ${ store . get ( "khojToken" ) } ` } ;
let requests = [ ] ;
// Request indexing files on server. With upto 1000 files in each request
for ( let i = 0 ; i < filesDataToPush . length ; i += 1000 ) {
const filesDataGroup = filesDataToPush . slice ( i , i + 1000 ) ;
const formData = new FormData ( ) ;
filesDataGroup . forEach ( fileData => { formData . append ( 'files' , fileData . blob , fileData . path ) } ) ;
let request = axios . post ( ` ${ hostURL } /api/v1/index/update?force= ${ regenerate } &client=desktop ` , formData , { headers } ) ;
requests . push ( request ) ;
}
// Wait for requests batch to finish
Promise
. all ( requests )
. then ( responses => {
const lastSync = filesToPush
. filter ( file => responses . find ( response => response . data . includes ( file ) ) )
. map ( file => ( { path : file , datetime : new Date ( ) . toISOString ( ) } ) ) ;
store . set ( 'lastSync' , lastSync ) ;
} )
. catch ( error => {
console . error ( error ) ;
state [ "completed" ] = false ;
2024-02-26 19:53:36 +00:00
if ( error ? . response ? . status === 429 && ( BrowserWindow . getAllWindows ( ) . find ( win => win . webContents . getURL ( ) . includes ( 'config' ) ) ) ) {
2024-07-17 07:29:06 +00:00
state [ "error" ] = ` Looks like you're out of space to sync your files. <a href="https://app.khoj.dev/settings">Upgrade your plan</a> to unlock more space. ` ;
2024-02-26 19:53:36 +00:00
const win = BrowserWindow . getAllWindows ( ) . find ( win => win . webContents . getURL ( ) . includes ( 'config' ) ) ;
if ( win ) win . webContents . send ( 'needsSubscription' , true ) ;
2024-01-09 17:02:06 +00:00
} else if ( error ? . code === 'ECONNREFUSED' ) {
state [ "error" ] = ` Could not connect to Khoj server. Ensure you can connect to it at ${ error . address } : ${ error . port } . ` ;
} else {
2024-04-02 20:19:15 +00:00
currentTime = new Date ( ) ;
2024-01-09 17:02:06 +00:00
state [ "error" ] = ` Sync was unsuccessful at ${ currentTime . toLocaleTimeString ( ) } . Contact team@khoj.dev to report this issue. ` ;
2024-01-03 17:38:20 +00:00
}
} )
. finally ( ( ) => {
2023-10-18 08:00:41 +00:00
// Syncing complete
2023-11-09 01:59:02 +00:00
syncing = false ;
2024-02-11 10:35:28 +00:00
const win = BrowserWindow . getAllWindows ( ) . find ( win => win . webContents . getURL ( ) . includes ( 'config' ) ) ;
if ( win ) {
2024-01-09 17:02:06 +00:00
win . webContents . send ( 'update-state' , state ) ;
}
2024-01-03 17:38:20 +00:00
} ) ;
2023-09-06 19:04:18 +00:00
}
pushDataToKhoj ( ) ;
2023-10-03 18:43:19 +00:00
async function handleFileOpen ( type ) {
let { canceled , filePaths } = { canceled : true , filePaths : [ ] } ;
if ( type === 'file' ) {
2024-04-09 10:37:18 +00:00
( { canceled , filePaths } = await dialog . showOpenDialog ( { properties : [ 'openFile' ] , filters : [ { name : "Valid Khoj Files" , extensions : validFileTypes } ] } ) ) ;
2023-10-03 18:43:19 +00:00
} else if ( type === 'folder' ) {
( { canceled , filePaths } = await dialog . showOpenDialog ( { properties : [ 'openDirectory' ] } ) ) ;
}
2023-09-06 19:04:18 +00:00
if ( ! canceled ) {
const files = store . get ( 'files' ) || [ ] ;
const folders = store . get ( 'folders' ) || [ ] ;
for ( const filePath of filePaths ) {
console . log ( filePath ) ;
if ( fs . existsSync ( filePath ) ) {
const stats = fs . statSync ( filePath ) ;
if ( stats . isFile ( ) ) {
console . log ( ` ${ filePath } is a file. ` ) ;
if ( files . find ( ( file ) => file . path === filePath ) ) {
continue ;
}
files . push ( { path : filePath } ) ;
store . set ( 'files' , files ) ;
} else if ( stats . isDirectory ( ) ) {
console . log ( ` ${ filePath } is a directory. ` ) ;
if ( folders . find ( ( folder ) => folder . path === filePath ) ) {
continue ;
}
folders . push ( { path : filePath } ) ;
store . set ( 'folders' , folders ) ;
}
} else {
console . log ( ` ${ filePath } does not exist. ` ) ;
}
}
return {
files : store . get ( 'files' ) ,
folders : store . get ( 'folders' )
}
}
}
2023-10-26 19:33:03 +00:00
async function getToken ( ) {
return store . get ( 'khojToken' ) ;
}
async function setToken ( event , token ) {
store . set ( 'khojToken' , token ) ;
return store . get ( 'khojToken' ) ;
}
2023-09-06 19:04:18 +00:00
async function getFiles ( ) {
return store . get ( 'files' ) ;
}
async function getFolders ( ) {
return store . get ( 'folders' ) ;
}
async function setURL ( event , url ) {
2023-11-01 00:59:53 +00:00
// Sanitize the URL. Remove trailing slash if present. Add http:// if not present.
url = url . replace ( /\/$/ , "" ) ;
if ( ! url . match ( /^[a-zA-Z]+:\/\// ) ) {
url = ` http:// ${ url } ` ;
}
2023-09-06 19:04:18 +00:00
store . set ( 'hostURL' , url ) ;
return store . get ( 'hostURL' ) ;
}
async function getURL ( ) {
return store . get ( 'hostURL' ) ;
}
async function removeFile ( event , filePath ) {
const files = store . get ( 'files' ) ;
const newFiles = files . filter ( ( file ) => file . path !== filePath ) ;
store . set ( 'files' , newFiles ) ;
return newFiles ;
}
async function removeFolder ( event , folderPath ) {
const folders = store . get ( 'folders' ) ;
const newFolders = folders . filter ( ( folder ) => folder . path !== folderPath ) ;
store . set ( 'folders' , newFolders ) ;
return newFolders ;
}
2023-09-12 23:35:07 +00:00
async function syncData ( regenerate = false ) {
2023-09-06 19:04:18 +00:00
try {
2024-04-12 06:19:25 +00:00
pushDataToKhoj ( regenerate ) ;
2023-09-06 19:04:18 +00:00
const date = new Date ( ) ;
console . log ( 'Pushing data to Khoj at: ' , date ) ;
} catch ( err ) {
console . error ( err ) ;
}
}
2023-11-07 11:37:16 +00:00
async function deleteAllFiles ( ) {
try {
store . set ( 'files' , [ ] ) ;
store . set ( 'folders' , [ ] ) ;
2024-04-12 06:19:25 +00:00
pushDataToKhoj ( true ) ;
2023-11-07 11:37:16 +00:00
const date = new Date ( ) ;
console . log ( 'Pushing data to Khoj at: ' , date ) ;
} catch ( err ) {
console . error ( err ) ;
}
}
2024-04-07 15:28:43 +00:00
// Fetch user info from Khoj server
async function getUserInfo ( ) {
const getUserInfoURL = ` ${ store . get ( 'hostURL' ) || KHOJ _URL } /api/v1/user?client=desktop ` ;
const headers = { 'Authorization' : ` Bearer ${ store . get ( 'khojToken' ) } ` } ;
try {
let response = await axios . get ( getUserInfoURL , { headers } ) ;
return response . data ;
} catch ( err ) {
console . error ( err ) ;
}
}
2023-11-07 11:37:16 +00:00
Enforce Content-Security-Policy (CSP) in Obsidian, Desktop, Web apps
Prevent XSS attacks by enforcing Content-Security-Policy (CSP) in apps.
Do not allow loading images, other assets from untrusted domains.
- Only allow loading assets from trusted domains
like 'self', khoj.dev, ipapi for geolocation, google (fonts, img)
- images from khoj domain, google (for profile pic)
- assets from khoj domain
- Do not allow iframe src
- Allow unsafe-inline script and styles for now as markdown-it escapes html
in user, khoj chat
- Add hostURL to CSP of the Desktop, Obsidian apps
Given web client is served by khoj server, it doesn't need to
explicitly allow for khoj.dev domain. So if user self-hosting, it'll
automatically allow the domain in the CSP (via 'self')
Whereas the Obsidian, Desktop clients allow configure the server URL.
Note *switching server URL breaks CSP until app is reloaded*
2024-02-20 21:42:00 +00:00
function addCSPHeaderToSession ( ) {
// Get hostURL from store or use default
const hostURL = store . get ( 'hostURL' ) || KHOJ _URL ;
// Construct Content Security Policy
const defaultDomains = ` 'self' ${ hostURL } https://app.khoj.dev https://assets.khoj.dev ` ;
const default _src = ` default-src ${ defaultDomains } ; ` ;
const script _src = ` script-src ${ defaultDomains } 'unsafe-inline'; ` ;
const connect _src = ` connect-src ${ hostURL } https://ipapi.co/json; ` ;
const style _src = ` style-src ${ defaultDomains } 'unsafe-inline' https://fonts.googleapis.com; ` ;
const img _src = ` img-src ${ defaultDomains } data: https://*.khoj.dev https://*.googleusercontent.com; ` ;
const font _src = ` font-src https://fonts.gstatic.com; ` ;
const child _src = ` child-src 'none'; ` ;
const objectSrc = ` object-src 'none'; ` ;
const csp = ` ${ default _src } ${ script _src } ${ connect _src } ${ style _src } ${ img _src } ${ font _src } ${ child _src } ${ objectSrc } ` ;
// Add Content Security Policy to all web requests
session . defaultSession . webRequest . onHeadersReceived ( ( details , callback ) => {
callback ( {
responseHeaders : {
... details . responseHeaders ,
'Content-Security-Policy' : [ csp ]
}
} )
} )
}
2023-11-03 09:46:39 +00:00
let firstRun = true ;
2023-10-26 19:33:03 +00:00
let win = null ;
2024-04-07 06:21:54 +00:00
let titleBarStyle = process . platform === 'win32' ? 'default' : 'hidden' ;
2024-06-27 14:20:13 +00:00
const { globalShortcut , clipboard } = require ( 'electron' ) ; // global shortcut and clipboard dependencies for shortcut window
const openShortcutWindowKeyBind = 'CommandOrControl+Shift+K'
2023-11-03 03:40:35 +00:00
const createWindow = ( tab = 'chat.html' ) => {
2023-10-26 19:33:03 +00:00
win = new BrowserWindow ( {
2023-09-06 19:04:18 +00:00
width : 800 ,
height : 800 ,
2023-11-03 09:46:39 +00:00
show : false ,
2024-04-07 06:21:54 +00:00
titleBarStyle : titleBarStyle ,
autoHideMenuBar : true ,
2023-09-06 19:04:18 +00:00
webPreferences : {
preload : path . join ( _ _dirname , 'preload.js' ) ,
nodeIntegration : true ,
}
} )
2024-04-12 06:19:25 +00:00
const job = new cron ( '0 */10 * * * *' , function ( ) {
2023-09-06 19:04:18 +00:00
try {
2024-04-12 06:19:25 +00:00
pushDataToKhoj ( ) ;
2023-09-06 19:04:18 +00:00
const date = new Date ( ) ;
console . log ( 'Pushing data to Khoj at: ' , date ) ;
win . webContents . send ( 'update-state' , state ) ;
} catch ( err ) {
console . error ( err ) ;
}
} ) ;
win . setResizable ( true ) ;
win . setOpacity ( 0.95 ) ;
2023-11-03 09:46:39 +00:00
win . setBackgroundColor ( '#f5f4f3' ) ;
2023-09-06 19:04:18 +00:00
win . setHasShadow ( true ) ;
2024-02-24 22:22:06 +00:00
// Open external links in link handler registered on OS (e.g. browser)
win . webContents . setWindowOpenHandler ( async ( { url } ) => {
2024-03-13 20:59:22 +00:00
let shouldOpen = { response : 0 } ;
2024-02-24 22:22:06 +00:00
2024-03-13 20:59:22 +00:00
if ( ! url . startsWith ( store . get ( 'hostURL' ) ) ) {
// Confirm before opening external links
2024-02-24 22:22:06 +00:00
const confirmNotice = ` Do you want to open this link? It will be handled by an external application. \n \n ${ url } ` ;
shouldOpen = await dialog . showMessageBox ( {
type : 'question' ,
buttons : [ 'Yes' , 'No' ] ,
defaultId : 1 ,
title : 'Confirm' ,
message : confirmNotice ,
} ) ;
}
// If user confirms, let OS link handler open the link in appropriate app
if ( shouldOpen . response === 0 ) shell . openExternal ( url ) ;
// Do not open external links within the app
return { action : 'deny' } ;
} ) ;
2023-09-06 19:04:18 +00:00
job . start ( ) ;
2023-10-26 19:33:03 +00:00
win . loadFile ( tab )
2023-11-03 09:46:39 +00:00
if ( firstRun === true ) {
firstRun = false ;
// Create splash screen
2023-12-29 04:50:48 +00:00
let splash = new BrowserWindow ( { width : 400 , height : 400 , transparent : true , frame : false , alwaysOnTop : true } ) ;
2023-11-04 05:20:11 +00:00
splash . setOpacity ( 1.0 ) ;
2023-11-03 09:46:39 +00:00
splash . setBackgroundColor ( '#d16b4e' ) ;
splash . loadFile ( 'splash.html' ) ;
// Show splash screen on app load
win . once ( 'ready-to-show' , ( ) => {
setTimeout ( function ( ) { splash . close ( ) ; win . show ( ) ; } , 4500 ) ;
} ) ;
} else {
// Show main window directly if not first run
win . once ( 'ready-to-show' , ( ) => { win . show ( ) ; } ) ;
}
2023-09-06 19:04:18 +00:00
}
2024-06-27 14:20:13 +00:00
const createShortcutWindow = ( tab = 'shortcut.html' ) => {
var shortcutWin = new BrowserWindow ( {
width : 400 ,
height : 600 ,
show : false ,
titleBarStyle : titleBarStyle ,
autoHideMenuBar : true ,
frame : false ,
webPreferences : {
preload : path . join ( _ _dirname , 'preload.js' ) ,
nodeIntegration : true ,
}
} ) ;
shortcutWin . setMenuBarVisibility ( false ) ;
shortcutWin . setResizable ( false ) ;
shortcutWin . setOpacity ( 0.95 ) ;
shortcutWin . setBackgroundColor ( '#f5f4f3' ) ;
shortcutWin . setHasShadow ( true ) ;
shortcutWin . setVibrancy ( 'popover' ) ;
shortcutWin . loadFile ( tab ) ;
shortcutWin . once ( 'ready-to-show' , ( ) => {
shortcutWin . show ( ) ;
} ) ;
shortcutWin . on ( 'closed' , ( ) => {
shortcutWin = null ;
} ) ;
return shortcutWin ;
} ;
function isShortcutWindowOpen ( ) {
const windows = BrowserWindow . getAllWindows ( ) ;
for ( let i = 0 ; i < windows . length ; i ++ ) {
if ( windows [ i ] . webContents . getURL ( ) . endsWith ( 'shortcut.html' ) ) {
return true ;
}
}
return false ;
}
2023-09-06 19:04:18 +00:00
app . whenReady ( ) . then ( ( ) => {
Enforce Content-Security-Policy (CSP) in Obsidian, Desktop, Web apps
Prevent XSS attacks by enforcing Content-Security-Policy (CSP) in apps.
Do not allow loading images, other assets from untrusted domains.
- Only allow loading assets from trusted domains
like 'self', khoj.dev, ipapi for geolocation, google (fonts, img)
- images from khoj domain, google (for profile pic)
- assets from khoj domain
- Do not allow iframe src
- Allow unsafe-inline script and styles for now as markdown-it escapes html
in user, khoj chat
- Add hostURL to CSP of the Desktop, Obsidian apps
Given web client is served by khoj server, it doesn't need to
explicitly allow for khoj.dev domain. So if user self-hosting, it'll
automatically allow the domain in the CSP (via 'self')
Whereas the Obsidian, Desktop clients allow configure the server URL.
Note *switching server URL breaks CSP until app is reloaded*
2024-02-20 21:42:00 +00:00
addCSPHeaderToSession ( ) ;
2023-09-06 19:04:18 +00:00
ipcMain . on ( 'set-title' , handleSetTitle ) ;
2023-10-03 18:43:19 +00:00
ipcMain . handle ( 'handleFileOpen' , ( event , type ) => {
return handleFileOpen ( type ) ;
} ) ;
2023-09-06 19:04:18 +00:00
ipcMain . on ( 'update-state' , ( event , arg ) => {
console . log ( arg ) ;
event . reply ( 'update-state' , arg ) ;
} ) ;
2023-11-26 06:31:23 +00:00
ipcMain . on ( 'needsSubscription' , ( event , arg ) => {
console . log ( arg ) ;
event . reply ( 'needsSubscription' , arg ) ;
} ) ;
2023-11-16 06:33:50 +00:00
ipcMain . on ( 'navigate' , ( event , page ) => {
win . loadFile ( page ) ;
} ) ;
ipcMain . on ( 'navigateToWebApp' , ( event , page ) => {
shell . openExternal ( ` ${ store . get ( 'hostURL' ) } / ${ page } ` ) ;
} ) ;
2023-09-06 19:04:18 +00:00
ipcMain . handle ( 'getFiles' , getFiles ) ;
ipcMain . handle ( 'getFolders' , getFolders ) ;
ipcMain . handle ( 'removeFile' , removeFile ) ;
ipcMain . handle ( 'removeFolder' , removeFolder ) ;
ipcMain . handle ( 'setURL' , setURL ) ;
ipcMain . handle ( 'getURL' , getURL ) ;
2023-10-26 19:33:03 +00:00
ipcMain . handle ( 'setToken' , setToken ) ;
ipcMain . handle ( 'getToken' , getToken ) ;
2024-04-07 15:28:43 +00:00
ipcMain . handle ( 'getUserInfo' , getUserInfo ) ;
2023-10-26 19:33:03 +00:00
2023-09-12 23:35:07 +00:00
ipcMain . handle ( 'syncData' , ( event , regenerate ) => {
syncData ( regenerate ) ;
} ) ;
2023-11-07 11:37:16 +00:00
ipcMain . handle ( 'deleteAllFiles' , deleteAllFiles ) ;
2023-09-06 19:04:18 +00:00
2024-06-27 14:20:13 +00:00
const mainWindow = createWindow ( ) ;
2023-09-06 19:04:18 +00:00
app . setAboutPanelOptions ( {
applicationName : "Khoj" ,
2023-11-04 03:56:27 +00:00
applicationVersion : khojPackage . version ,
version : khojPackage . version ,
2024-06-27 14:20:13 +00:00
authors : "Khoj AI" ,
2023-09-06 19:04:18 +00:00
website : "https://khoj.dev" ,
2023-11-04 03:56:27 +00:00
copyright : "GPL v3" ,
iconPath : path . join ( _ _dirname , 'assets' , 'icons' , 'favicon-128x128.png' )
2023-09-06 19:04:18 +00:00
} ) ;
app . on ( 'ready' , async ( ) => {
try {
const result = await todesktop . autoUpdater . checkForUpdates ( ) ;
if ( result . updateInfo ) {
2024-04-09 10:33:25 +00:00
console . log ( "Desktop app update found:" , result . updateInfo . version ) ;
2023-09-06 19:04:18 +00:00
todesktop . autoUpdater . restartAndInstall ( ) ;
}
} catch ( e ) {
2024-04-09 10:33:25 +00:00
console . warn ( "Desktop app update check failed:" , e ) ;
2023-09-06 19:04:18 +00:00
}
} )
2024-06-27 14:20:13 +00:00
globalShortcut . register ( openShortcutWindowKeyBind , ( ) => {
console . log ( "Shortcut key pressed" )
if ( isShortcutWindowOpen ( ) ) return ;
const shortcutWin = createShortcutWindow ( ) ; // Create a new shortcut window each time the shortcut is triggered
shortcutWin . setAlwaysOnTop ( true , 'screen-saver' , 1 ) ;
const clipboardText = clipboard . readText ( ) ;
console . log ( 'Sending clipboard text:' , clipboardText ) ; // Debug log
shortcutWin . webContents . once ( 'dom-ready' , ( ) => {
shortcutWin . webContents . send ( 'clip' , clipboardText ) ;
console . log ( 'Message sent to window' ) ; // Debug log
} ) ;
// Register a global shortcut for the Escape key for the shortcutWin
globalShortcut . register ( 'Escape' , ( ) => {
if ( shortcutWin ) {
shortcutWin . close ( ) ;
}
// Unregister the Escape key shortcut
globalShortcut . unregister ( 'Escape' ) ;
} ) ;
2023-09-06 19:04:18 +00:00
2024-06-27 14:20:13 +00:00
shortcutWin . on ( 'closed' , ( ) => {
// Unregister the Escape key shortcut
globalShortcut . unregister ( 'Escape' ) ;
} ) ;
ipcMain . on ( 'continue-conversation-button-clicked' , ( ) => {
openWindow ( 'chat.html' ) ;
if ( shortcutWin && ! shortcutWin . isDestroyed ( ) ) {
shortcutWin . close ( ) ;
}
// Unregister the Escape key shortcut
globalShortcut . unregister ( 'Escape' ) ;
} ) ;
} ) ;
2023-09-06 19:04:18 +00:00
app . on ( 'activate' , ( ) => {
2024-06-27 14:20:13 +00:00
if ( BrowserWindow . getAllWindows ( ) . length === 0 ) createWindow ( )
2023-09-06 19:04:18 +00:00
} )
} )
app . on ( 'window-all-closed' , ( ) => {
if ( process . platform !== 'darwin' ) app . quit ( )
} )
2023-10-26 19:33:03 +00:00
2023-11-04 03:56:27 +00:00
/ *
* * About Page
* /
let aboutWindow ;
function openAboutWindow ( ) {
if ( aboutWindow ) { aboutWindow . focus ( ) ; return ; }
aboutWindow = new BrowserWindow ( {
width : 400 ,
height : 400 ,
2024-04-07 06:21:54 +00:00
titleBarStyle : titleBarStyle ,
autoHideMenuBar : true ,
2023-11-04 03:56:27 +00:00
show : false ,
webPreferences : {
preload : path . join ( _ _dirname , 'preload.js' ) ,
nodeIntegration : true ,
} ,
} ) ;
aboutWindow . loadFile ( 'about.html' ) ;
// Pass OS, Khoj version to About page
aboutWindow . webContents . on ( 'did-finish-load' , ( ) => {
aboutWindow . webContents . send ( 'appInfo' , { version : khojPackage . version , platform : process . platform } ) ;
} ) ;
// Open links in external browser
aboutWindow . webContents . setWindowOpenHandler ( ( { url } ) => {
shell . openExternal ( url ) ;
return { action : 'deny' } ;
} ) ;
aboutWindow . once ( 'ready-to-show' , ( ) => { aboutWindow . show ( ) ; } ) ;
aboutWindow . on ( 'closed' , ( ) => { aboutWindow = null ; } ) ;
}
2023-10-26 19:33:03 +00:00
/ *
* * System Tray Icon
* /
let tray
openWindow = ( page ) => {
if ( BrowserWindow . getAllWindows ( ) . length === 0 ) {
createWindow ( page ) ;
} else {
win . loadFile ( page ) ; win . show ( ) ;
}
}
app . whenReady ( ) . then ( ( ) => {
2023-12-03 07:52:01 +00:00
const iconPath = path . join ( _ _dirname , './assets/icons/favicon-20x20.png' )
const icon = nativeImage . createFromPath ( iconPath )
2023-10-26 19:33:03 +00:00
tray = new Tray ( icon )
const contextMenu = Menu . buildFromTemplate ( [
{ label : 'Chat' , type : 'normal' , click : ( ) => { openWindow ( 'chat.html' ) ; } } ,
2023-11-03 03:40:35 +00:00
{ label : 'Search' , type : 'normal' , click : ( ) => { openWindow ( 'search.html' ) } } ,
2023-10-26 19:33:03 +00:00
{ label : 'Configure' , type : 'normal' , click : ( ) => { openWindow ( 'config.html' ) } } ,
{ type : 'separator' } ,
2023-11-04 03:56:27 +00:00
{ label : 'About Khoj' , type : 'normal' , click : ( ) => { openAboutWindow ( ) ; } } ,
2023-10-26 19:33:03 +00:00
{ label : 'Quit' , type : 'normal' , click : ( ) => { app . quit ( ) } }
] )
tray . setToolTip ( 'Khoj' )
tray . setContextMenu ( contextMenu )
} )