2019-06-26 16:09:01 -04:00
import { is_debug } from "../main_window" ;
const request = require ( 'request' ) ;
const querystring = require ( 'querystring' ) ;
const fs = require ( 'fs-extra' ) ;
const os = require ( 'os' ) ;
const UUID = require ( 'pure-uuid' ) ;
import * as path from "path" ;
import * as zlib from "zlib" ;
import * as tar from "tar-stream" ;
import { Arguments , process_args } from "../../shared/process-arguments" ;
import { parse_version } from "../../shared/version" ;
import * as electron from "electron" ;
import MessageBoxOptions = Electron . MessageBoxOptions ;
import { current_version , execute_graphical } from "../app-updater" ;
const TIMEOUT = 10000 ;
let local_path = undefined ;
interface RemoteURL {
( ) : string ;
cached? : string ;
}
const remote_url : RemoteURL = ( ) = > {
if ( remote_url . cached )
return remote_url . cached ;
const default_path = is_debug ? "http://localhost/home/TeaSpeak/Web-Client/client-api/environment/" : "https://clientapi.teaspeak.de/" ;
return remote_url . cached = ( process_args . has_value ( . . . Arguments . SERVER_URL ) ? process_args . value ( . . . Arguments . SERVER_URL ) : default_path ) ;
} ;
function data_directory ( ) : string {
return electron . app . getPath ( 'userData' ) ;
}
function cache_directory ( ) : string {
return path . join ( data_directory ( ) , "cache" , "ui" ) ;
}
function working_directory ( ) : string {
return path . join ( data_directory ( ) , "tmp" , "ui" ) ;
}
export interface VersionedFile {
name : string ,
hash : string ,
path : string ,
type : string ,
local_url : ( ) = > Promise < String >
}
function generate_tmp ( ) : Promise < String > {
if ( local_path ) return Promise . resolve ( local_path ) ;
const id = new UUID ( 4 ) . format ( ) ;
const directory = path . join ( os . tmpdir ( ) , "TeaClient-" + id ) + "/" ;
return fs . mkdirs ( directory ) . then ( ( ) = > {
local_path = directory ;
global [ "browser-root" ] = local_path ;
console . log ( "Local browser path: %s" , local_path ) ;
return Promise . resolve ( local_path ) ;
} ) ;
}
function get_raw_app_files ( ) : Promise < VersionedFile [ ] > {
return generate_tmp ( ) . then ( path = > new Promise < VersionedFile [ ] > ( ( resolve , reject ) = > {
const url = remote_url ( ) + "api.php?" + querystring . stringify ( {
type : "files" ,
} ) ;
console . debug ( "Requesting file list from %s" , url ) ;
request . get ( url , {
timeout : TIMEOUT
} , ( error , response , body : string ) = > {
response = response || { statusCode : - 1 } ;
if ( error ) { reject ( error ) ; return ; }
if ( response . statusCode != 200 ) { setImmediate ( reject , "invalid status code " + response . statusCode + " for " + url ) ; return ; }
if ( response . headers [ "info-version" ] != 1 ) { setImmediate ( reject , "Invalid response version (" + response . headers [ "info-version" ] + "). Update your app manually!" ) ; return ; }
if ( ! body ) {
setImmediate ( reject , "invalid body. (Missing)" ) ;
return ;
}
let result : VersionedFile [ ] = [ ] ;
body . split ( "\n" ) . forEach ( entry = > {
if ( entry . length == 0 ) return ;
let info = entry . split ( "\t" ) ;
if ( info [ 0 ] == "type" ) return ;
result . push ( {
type : info [ 0 ] ,
hash : info [ 1 ] ,
path : info [ 2 ] ,
name : info [ 3 ]
} as VersionedFile ) ;
} ) ;
setImmediate ( resolve , result ) ;
} ) ;
} )
) ;
}
function download_raw_app_files ( ) : Promise < VersionedFile [ ] > {
return get_raw_app_files ( ) . then ( response = > {
for ( let file of response ) {
file . local_url = ( ) = > fs . mkdirs ( local_path + file . path + "/" ) . then ( ( ) = > new Promise < String > ( ( resolve , reject ) = > {
request . get ( remote_url ( ) + "api.php?" + querystring . stringify ( {
type : "file" ,
path : file.path ,
name : file.name
} ) , {
timeout : TIMEOUT
} ) . on ( 'response' , function ( response ) {
if ( response . statusCode != 200 ) {
setImmediate ( reject , "invalid status code " + response . statusCode + " for file " + file . name + " (" + file . path + ")" ) ;
return ;
}
} ) . on ( 'complete' , event = > {
} ) . on ( 'error' , error = > {
setImmediate ( reject , error ) ;
} ) . pipe ( fs . createWriteStream ( local_path + file . path + "/" + file . name ) )
. on ( 'finish' , event = > {
setImmediate ( resolve , file . path + "/" + file . name ) ;
} ) ;
} ) ) ;
}
return Promise . resolve ( response ) ;
} ) . catch ( error = > {
console . log ( "Failed to get file list: %o" , error ) ;
return Promise . reject ( "Failed to get file list (" + error + ")" ) ;
} )
}
interface LocalUICache {
fetch_history? : FetchStatus ;
versions? : LocalUICacheEntry [ ] ;
remote_index? : UIVersion [ ] | UIVersion ;
remote_index_channel? : string ; /* only set if the last status was a channel only*/
local_index? : UIVersion ;
}
interface FetchStatus {
timestamp : number ;
/ * *
* 0 = success
* 1 = connect fail
* 2 = internal fail
* /
status : number ;
}
interface LocalUICacheEntry {
version : UIVersion ;
download_timestamp : number ;
tar_file : string ;
checksum : string ; /* SHA512 */
}
export interface UIVersion {
channel : string ;
version : string ;
git_hash : string ;
timestamp : number ;
required_client? : string ;
filename? : string ;
client_shipped? : boolean ;
}
function ui_file_path ( version : UIVersion ) : string {
if ( version . client_shipped ) {
const app_path = electron . app . getAppPath ( ) ;
if ( ! app_path . endsWith ( ".asar" ) )
return undefined ;
return path . join ( path . join ( path . dirname ( app_path ) , "ui" ) , version . filename ) ;
}
const file_name = "ui_" + version . channel + "_" + version . version + "_" + version . git_hash + "_" + version . timestamp + ".tar.gz" ;
return path . join ( cache_directory ( ) , file_name ) ;
}
let _ui_load_cache : LocalUICache ;
async function ui_load_cache ( ) : Promise < LocalUICache > {
if ( _ui_load_cache ) return _ui_load_cache ;
const file = path . join ( cache_directory ( ) , "data.json" ) ;
if ( ! fs . existsSync ( file ) ) return { } as LocalUICache ;
console . log ( "Loading UI cache file %s" , file ) ;
_ui_load_cache = await fs . readJson ( file ) as LocalUICache ;
return _ui_load_cache ;
}
async function client_shipped_ui ( ) : Promise < UIVersion | undefined > {
const app_path = electron . app . getAppPath ( ) ;
if ( ! app_path . endsWith ( ".asar" ) )
return undefined ;
const base_path = path . join ( path . dirname ( app_path ) , "ui" ) ;
console . debug ( "Looking for client shipped UI pack at %s" , base_path ) ;
if ( ! ( await fs . pathExists ( base_path ) ) )
return undefined ;
const info : {
channel : string ,
version : string ,
git_hash : string ,
required_client : string ,
timestamp : number ,
filename : string
} = await fs . readJson ( path . join ( base_path , "default_ui_info.json" ) ) as any ;
return {
channel : info.channel ,
client_shipped : true ,
filename : info.filename ,
git_hash : info.git_hash ,
required_client : info.required_client ,
timestamp : info.timestamp ,
version : info.version ,
}
}
async function ui_save_cache ( cache : LocalUICache ) {
const file = path . join ( cache_directory ( ) , "data.json" ) ;
if ( ! fs . existsSync ( path . dirname ( file ) ) )
await fs . mkdirs ( path . dirname ( file ) ) ;
await fs . writeJson ( file , cache ) ;
}
async function get_ui_pack ( channel? : string ) : Promise < UIVersion [ ] | UIVersion > {
return await new Promise < UIVersion [ ] | UIVersion > ( ( resolve , reject ) = > {
const url = remote_url ( ) + "api.php?" + querystring . stringify ( {
type : "ui-info"
} ) ;
request . get ( url , {
timeout : TIMEOUT
} , ( error , response , body : string ) = > {
try {
response = response || { statusCode : - 1 } ;
if ( error ) { throw error ; }
if ( response . statusCode != 200 ) { throw "invalid status code " + response . statusCode + " for " + url ; }
if ( ! body ) throw "invalid response body" ;
let result : UIVersion [ ] = [ ] ;
const json = JSON . parse ( body ) || { success : false , msg : "invalid body" } ;
if ( ! json [ "success" ] ) throw "Failed to get ui info: " + json [ "msg" ] ;
for ( const entry of json [ "versions" ] ) {
if ( ! channel || entry [ "channel" ] == channel )
result . push ( {
channel : entry [ "channel" ] ,
version : entry [ "version" ] ,
git_hash : entry [ "git-ref" ] ,
timestamp : entry [ "timestamp" ] ,
required_client : entry [ "required_client" ]
} ) ;
}
if ( result . length == 0 && channel ) result . push ( undefined ) ;
const res = channel ? result [ 0 ] : result ;
ui_load_cache ( ) . then ( async cache = > {
cache . fetch_history = cache . fetch_history || { } as any ;
cache . fetch_history . timestamp = Date . now ( ) ;
cache . fetch_history . status = 0 ;
cache . remote_index = res as any ;
cache . remote_index_channel = channel ;
await ui_save_cache ( cache ) ;
} ) . catch ( error = > {
console . warn ( "Failed to save UI cache info: %o" , error ) ;
resolve ( res ) ;
} ) . then ( err = > resolve ( res ) ) ;
} catch ( error ) {
reject ( error ) ;
}
} ) ;
} )
}
async function download_ui_pack ( version : UIVersion ) : Promise < void > {
const directory = cache_directory ( ) ;
const file = ui_file_path ( version ) ;
await fs . mkdirs ( directory ) ;
await new Promise ( ( resolve , reject ) = > {
request . get ( remote_url ( ) + "api.php?" + querystring . stringify ( {
type : "ui-download" ,
"git-ref" : version . git_hash ,
version : version.version ,
timestamp : version.timestamp ,
channel : version.channel
} ) , {
timeout : TIMEOUT
} ) . on ( 'response' , function ( response ) {
if ( response . statusCode != 200 ) { reject ( "Failed to download UI files (Status code " + response . statusCode + ")" ) ; }
} ) . on ( 'error' , error = > {
reject ( "Failed to download UI files: " + error ) ;
} ) . pipe ( fs . createWriteStream ( file ) ) . on ( 'finish' , ( ) = > {
ui_load_cache ( ) . then ( cache = > {
cache . versions . push ( {
checksum : "undefined" ,
tar_file : file ,
download_timestamp : Date.now ( ) ,
version : version
} ) ;
return ui_save_cache ( cache ) ;
} ) . catch ( error = > resolve ( ) ) . then ( ( ) = > resolve ( ) ) ;
} ) ;
} ) ;
}
function ui_pack_exists ( version : UIVersion ) : boolean {
return fs . existsSync ( ui_file_path ( version ) ) ;
}
async function unpack_cached ( version : UIVersion ) : Promise < string > {
const file = ui_file_path ( version ) ;
if ( ! fs . existsSync ( file ) ) throw "missing file" ;
const target_dir = path . join ( working_directory ( ) , version . channel + "_" + version . timestamp ) ;
if ( fs . existsSync ( target_dir ) ) fs . removeSync ( target_dir ) ;
await fs . mkdirs ( target_dir ) ;
const gunzip = zlib . createGunzip ( ) ;
const extract = tar . extract ( ) ;
const fpipe = fs . createReadStream ( file ) ;
extract . on ( 'entry' , function ( header : tar.Headers , stream , next ) {
if ( header . type == 'file' ) {
const target_file = path . join ( target_dir , header . name ) ;
if ( ! fs . existsSync ( path . dirname ( target_file ) ) ) fs . mkdirsSync ( path . dirname ( target_file ) ) ;
stream . on ( 'end' , ( ) = > setImmediate ( next ) ) ;
const wfpipe = fs . createWriteStream ( target_file ) ;
stream . pipe ( wfpipe ) ;
} else if ( header . type == 'directory' ) {
if ( fs . existsSync ( path . join ( target_dir , header . name ) ) )
setImmediate ( next ) ;
fs . mkdirs ( path . join ( target_dir , header . name ) ) . catch ( error = > {
console . warn ( "Failed to create unpacking fir " + path . join ( target_dir , header . name ) ) ;
console . error ( error ) ;
} ) . then ( ( ) = > setImmediate ( next ) ) ;
} else {
console . warn ( "Invalid ui tar ball entry type (" + header . type + ")" ) ;
return ;
}
} ) ;
const finish_promise = new Promise ( resolve = > {
extract . on ( 'finish' , resolve ) ;
extract . on ( 'error' , event = > {
if ( ! event ) return ;
throw event ;
} ) ;
} ) ;
fpipe . pipe ( gunzip ) . pipe ( extract ) ;
await finish_promise ;
return target_dir ;
}
export async function cleanup() {
if ( await fs . pathExists ( local_path ) )
await fs . remove ( local_path ) ;
}
export async function load_files ( channel : string , static_cb : ( message : string , index : number ) = > any ) : Promise < String > {
const type = parseInt ( process_args . has_value ( Arguments . UPDATER_UI_LOAD_TYPE ) ? process_args . value ( Arguments . UPDATER_UI_LOAD_TYPE ) : "-1" ) ;
if ( type == 0 || ! is_debug ) {
console . log ( "Loading ui package" ) ;
static_cb ( "Fetching info" , 0 ) ;
const cache = await ui_load_cache ( ) ;
console . log ( "Local cache: %o" , cache ) ;
let ui_info : UIVersion ;
try {
ui_info = await get_ui_pack ( channel ) as UIVersion ;
} catch ( error ) {
if ( error instanceof Error )
console . error ( "Failed to fetch ui info: %s. Using cached info!" , error . message ) ;
else
console . error ( "Failed to fetch ui info: %o. Using cached info!" , error ) ;
}
if ( ! ui_info ) {
if ( cache && ! process_args . has_flag ( Arguments . UPDATER_UI_NO_CACHE ) ) {
if ( Array . isArray ( cache . remote_index ) ) {
for ( const index of cache . remote_index ) {
if ( index && index . channel == "release" ) {
ui_info = index ;
break ;
}
}
} else {
//TODO: test channel?
ui_info = cache . remote_index ;
}
}
if ( ui_info ) {
console . debug ( "Found local UI pack." ) ;
} else {
//Test for the client shipped ui pack
try {
console . info ( "Looking for client shipped UI pack." ) ;
ui_info = await client_shipped_ui ( ) ;
if ( ! ui_info )
throw "failed to load info" ;
console . info ( "Using client shipped UI pack because we've no active internet connection." )
} catch ( error ) {
console . warn ( "Failed to load client shipped UI pack: %o" , error ) ;
throw "Failed to load UI pack from cache!\nPlease ensure a valid internet connection." ;
}
}
}
static_cb ( "Searching cache for file" , . 33 ) ;
console . log ( "Loading UI from data: %o. Target path: %s" , ui_info , ui_file_path ( ui_info ) ) ;
2019-07-24 14:27:10 -04:00
if ( ui_info . required_client && ! process_args . has_flag ( Arguments . DEBUG ) ) {
2019-06-26 16:09:01 -04:00
const ui_vers = parse_version ( ui_info . required_client ) ;
console . log ( "Checking required client version (Required: %s, Version: %s)" , ui_vers . toString ( true ) , ( await current_version ( ) ) . toString ( true ) ) ;
if ( ui_vers . newer_than ( await current_version ( ) ) ) {
const local_available = cache && cache . local_index ? ui_pack_exists ( cache . local_index ) : undefined ;
const result = electron . dialog . showMessageBox ( {
type : "question" ,
message :
"Local client is outdated.\n" +
"Newer UI packs (>= " + ui_info . version + ") require client " + ui_info . required_client + "\n" +
"Do you want to upgrade?" ,
title : "Client outdated!" ,
buttons : [ "yes" , local_available ? "ignore and use last possible (" + cache . local_index . version + ")" : "close client" ]
} as MessageBoxOptions ) ;
if ( result == 0 ) {
await execute_graphical ( channel , true ) ;
throw "client outdated" ;
} else {
if ( ! local_available ) {
electron . app . exit ( 1 ) ;
return ;
}
ui_info = cache . local_index ;
}
}
}
if ( ! ui_pack_exists ( ui_info ) ) {
console . log ( "Ui version does not locally exists. Downloading new one" ) ;
static_cb ( "Downloading files" , . 34 ) ;
await download_ui_pack ( ui_info ) ;
console . log ( "Download completed!" ) ;
}
console . log ( "Unpacking cached ui info" ) ;
static_cb ( "Unpacking files" , . 66 ) ;
const target_path = await unpack_cached ( ui_info ) ;
cache . local_index = ui_info ;
await ui_save_cache ( cache ) ;
console . log ( "Unpacked. Target path: %s" , target_path ) ;
static_cb ( "UI loaded" , 1 ) ;
return path . join ( target_path , "index.html" ) ;
} else {
console . log ( "Loading file by file" ) ;
static_cb ( "Fetching files" , 0 ) ;
let files ;
try {
files = await download_raw_app_files ( )
} catch ( error ) {
throw "Failed to get file list: " + error ;
}
console . log ( "Get raw files:" ) ;
let futures : Promise < void > [ ] = [ ] ;
let finish_count = 0 ;
static_cb ( "Downloading files" , 0 ) ;
for ( const file of files ) {
console . log ( "Start downloading %s (%s)" , file . name , file . path ) ;
const start = Date . now ( ) ;
futures . push ( file . local_url ( ) . then ( data = > {
finish_count ++ ;
console . log ( "Downloaded %s (%s) (%ims)" , file . name , file . path , Date . now ( ) - start ) ;
static_cb ( "Downloading files" , finish_count / files . length ) ;
} ) ) ;
//await new Promise(resolve => setTimeout(resolve, 1000));
}
try {
await Promise . all ( futures ) ;
} catch ( error ) {
throw "Failed to download files: " + error ;
}
return await generate_tmp ( ) + "index.html" ; /* entry point */
}
}