import { Inject, Injectable } from '@angular/core' import { HttpOptions, HttpService, isRpcError, Log, Method, RpcError, RPCOptions, SetupStatus, } from '@start9labs/shared' import { ApiService } from './embassy-api.service' import { BackupTargetType, Metrics, RR } from './api.types' import { ConfigService } from '../config.service' import { webSocket, WebSocketSubjectConfig } from 'rxjs/webSocket' import { Observable, filter, firstValueFrom } from 'rxjs' import { AuthService } from '../auth.service' import { DOCUMENT } from '@angular/common' import { DataModel } from '../patch-db/data-model' import { PatchDB, pathFromArray, Update } from 'patch-db-client' import { getServerInfo } from 'src/app/util/get-server-info' @Injectable() export class LiveApiService extends ApiService { constructor( @Inject(DOCUMENT) private readonly document: Document, private readonly http: HttpService, private readonly config: ConfigService, private readonly auth: AuthService, private readonly patch: PatchDB, ) { super() // @ts-ignore this.document.defaultView.rpcClient = this } // for getting static files: ex icons, instructions, licenses async getStatic(url: string): Promise { return this.httpRequest({ method: Method.GET, url, responseType: 'text', }) } // for sideloading packages async uploadPackage(guid: string, body: Blob): Promise { return this.httpRequest({ method: Method.POST, body, url: `/rest/rpc/${guid}`, responseType: 'text', }) } async uploadFile(body: Blob): Promise { return this.httpRequest({ method: Method.POST, body, url: `/rest/upload`, responseType: 'text', }) } // db async setDbValue( pathArr: Array, value: T, ): Promise { const pointer = pathFromArray(pathArr) const params: RR.SetDBValueReq = { pointer, value } return this.rpcRequest({ method: 'db.put.ui', params }) } // auth async login(params: RR.LoginReq): Promise { return this.rpcRequest({ method: 'auth.login', params }) } async logout(params: RR.LogoutReq): Promise { return this.rpcRequest({ method: 'auth.logout', params }) } async getSessions(params: RR.GetSessionsReq): Promise { return this.rpcRequest({ method: 'auth.session.list', params }) } async killSessions(params: RR.KillSessionsReq): Promise { return this.rpcRequest({ method: 'auth.session.kill', params }) } async resetPassword( params: RR.ResetPasswordReq, ): Promise { return this.rpcRequest({ method: 'auth.reset-password', params }) } // server async echo(params: RR.EchoReq, urlOverride?: string): Promise { return this.rpcRequest({ method: 'echo', params }, urlOverride) } openPatchWebsocket$(): Observable> { const config: WebSocketSubjectConfig> = { url: `/db`, closeObserver: { next: val => { if (val.reason === 'UNAUTHORIZED') this.auth.setUnverified() }, }, } return this.openWebsocket(config) } openLogsWebsocket$(config: WebSocketSubjectConfig): Observable { return this.openWebsocket(config) } openMetricsWebsocket$( config: WebSocketSubjectConfig, ): Observable { return this.openWebsocket(config) } async getSystemTime( params: RR.GetSystemTimeReq, ): Promise { return this.rpcRequest({ method: 'server.time', params }) } async getServerLogs( params: RR.GetServerLogsReq, ): Promise { return this.rpcRequest({ method: 'server.logs', params }) } async getKernelLogs( params: RR.GetServerLogsReq, ): Promise { return this.rpcRequest({ method: 'server.kernel-logs', params }) } async getTorLogs(params: RR.GetServerLogsReq): Promise { return this.rpcRequest({ method: 'net.tor.logs', params }) } async followServerLogs( params: RR.FollowServerLogsReq, ): Promise { return this.rpcRequest({ method: 'server.logs.follow', params }) } async followKernelLogs( params: RR.FollowServerLogsReq, ): Promise { return this.rpcRequest({ method: 'server.kernel-logs.follow', params }) } async followTorLogs( params: RR.FollowServerLogsReq, ): Promise { return this.rpcRequest({ method: 'net.tor.logs.follow', params }) } async getServerMetrics( params: RR.GetServerMetricsReq, ): Promise { return this.rpcRequest({ method: 'server.metrics', params }) } async updateServer(url?: string): Promise { const params = { marketplaceUrl: url || this.config.marketplace.start9, } return this.rpcRequest({ method: 'server.update', params }) } async setServerClearnetAddress( params: RR.SetServerClearnetAddressReq, ): Promise { return this.rpcRequest({ method: 'server.set-clearnet', params }) } async restartServer( params: RR.RestartServerReq, ): Promise { return this.rpcRequest({ method: 'server.restart', params }) } async shutdownServer( params: RR.ShutdownServerReq, ): Promise { return this.rpcRequest({ method: 'server.shutdown', params }) } async systemRebuild( params: RR.RestartServerReq, ): Promise { return this.rpcRequest({ method: 'server.rebuild', params }) } async repairDisk(params: RR.RestartServerReq): Promise { return this.rpcRequest({ method: 'disk.repair', params }) } async resetTor(params: RR.ResetTorReq): Promise { return this.rpcRequest({ method: 'net.tor.reset', params }) } async setOsOutboundProxy( params: RR.SetOsOutboundProxyReq, ): Promise { return this.rpcRequest({ method: 'server.proxy.set-outbound', params }) } // marketplace URLs async marketplaceProxy( path: string, qp: Record, baseUrl: string, ): Promise { const fullUrl = `${baseUrl}${path}?${new URLSearchParams(qp).toString()}` return this.rpcRequest({ method: 'marketplace.get', params: { url: fullUrl }, }) } async getEos(): Promise { const { id } = await getServerInfo(this.patch) const qp: RR.GetMarketplaceEosReq = { serverId: id } return this.marketplaceProxy( '/eos/v0/latest', qp, this.config.marketplace.start9, ) } // notification async getNotifications( params: RR.GetNotificationsReq, ): Promise { return this.rpcRequest({ method: 'notification.list', params }) } async deleteNotifications( params: RR.DeleteNotificationReq, ): Promise { return this.rpcRequest({ method: 'notification.delete', params }) } async markSeenNotifications( params: RR.MarkSeenNotificationReq, ): Promise { return this.rpcRequest({ method: 'notification.mark-seen', params }) } async markSeenAllNotifications( params: RR.MarkSeenAllNotificationsReq, ): Promise { return this.rpcRequest({ method: 'notification.mark-seen-before', params, }) } async markUnseenNotifications( params: RR.MarkUnseenNotificationReq, ): Promise { return this.rpcRequest({ method: 'notification.mark-unseen', params }) } // network async addProxy(params: RR.AddProxyReq): Promise { return this.rpcRequest({ method: 'net.proxy.add', params }) } async updateProxy(params: RR.UpdateProxyReq): Promise { return this.rpcRequest({ method: 'net.proxy.update', params }) } async deleteProxy(params: RR.DeleteProxyReq): Promise { return this.rpcRequest({ method: 'net.proxy.delete', params }) } // domains async claimStart9ToDomain( params: RR.ClaimStart9ToReq, ): Promise { return this.rpcRequest({ method: 'net.domain.me.claim', params }) } async deleteStart9ToDomain( params: RR.DeleteStart9ToReq, ): Promise { return this.rpcRequest({ method: 'net.domain.me.delete', params }) } async addDomain(params: RR.AddDomainReq): Promise { return this.rpcRequest({ method: 'net.domain.add', params }) } async deleteDomain(params: RR.DeleteDomainReq): Promise { return this.rpcRequest({ method: 'net.domain.delete', params }) } // port forwards async overridePortForward( params: RR.OverridePortReq, ): Promise { return this.rpcRequest({ method: 'net.port-forwards.override', params }) } // wifi async enableWifi(params: RR.EnableWifiReq): Promise { return this.rpcRequest({ method: 'wifi.enable', params }) } async getWifi( params: RR.GetWifiReq, timeout?: number, ): Promise { return this.rpcRequest({ method: 'wifi.get', params, timeout }) } async addWifi(params: RR.AddWifiReq): Promise { return this.rpcRequest({ method: 'wifi.add', params }) } async connectWifi(params: RR.ConnectWifiReq): Promise { return this.rpcRequest({ method: 'wifi.connect', params }) } async deleteWifi(params: RR.DeleteWifiReq): Promise { return this.rpcRequest({ method: 'wifi.delete', params }) } // email async testEmail(params: RR.TestEmailReq): Promise { return this.rpcRequest({ method: 'email.test', params }) } async configureEmail( params: RR.ConfigureEmailReq, ): Promise { return this.rpcRequest({ method: 'email.configure', params }) } // ssh async getSshKeys(params: RR.GetSSHKeysReq): Promise { return this.rpcRequest({ method: 'ssh.list', params }) } async addSshKey(params: RR.AddSSHKeyReq): Promise { return this.rpcRequest({ method: 'ssh.add', params }) } async deleteSshKey(params: RR.DeleteSSHKeyReq): Promise { return this.rpcRequest({ method: 'ssh.delete', params }) } // backup async getBackupTargets( params: RR.GetBackupTargetsReq, ): Promise { return this.rpcRequest({ method: 'backup.target.list', params }) } async addBackupTarget( type: BackupTargetType, params: RR.AddCifsBackupTargetReq | RR.AddCloudBackupTargetReq, ): Promise { params.path = params.path.replace('/\\/g', '/') return this.rpcRequest({ method: `backup.target.${type}.add`, params }) } async updateBackupTarget( type: BackupTargetType, params: RR.UpdateCifsBackupTargetReq | RR.UpdateCloudBackupTargetReq, ): Promise { return this.rpcRequest({ method: `backup.target.${type}.update`, params }) } async removeBackupTarget( params: RR.RemoveBackupTargetReq, ): Promise { return this.rpcRequest({ method: 'backup.target.remove', params }) } async getBackupJobs( params: RR.GetBackupJobsReq, ): Promise { return this.rpcRequest({ method: 'backup.job.list', params }) } async createBackupJob( params: RR.CreateBackupJobReq, ): Promise { return this.rpcRequest({ method: 'backup.job.create', params }) } async updateBackupJob( params: RR.UpdateBackupJobReq, ): Promise { return this.rpcRequest({ method: 'backup.job.update', params }) } async deleteBackupJob( params: RR.DeleteBackupJobReq, ): Promise { return this.rpcRequest({ method: 'backup.job.delete', params }) } async getBackupRuns( params: RR.GetBackupRunsReq, ): Promise { return this.rpcRequest({ method: 'backup.runs.list', params }) } async deleteBackupRuns( params: RR.DeleteBackupRunsReq, ): Promise { return this.rpcRequest({ method: 'backup.runs.delete', params }) } async getBackupInfo( params: RR.GetBackupInfoReq, ): Promise { return this.rpcRequest({ method: 'backup.target.info', params }) } async createBackup(params: RR.CreateBackupReq): Promise { return this.rpcRequest({ method: 'backup.create', params }) } // package async getPackageProperties( params: RR.GetPackagePropertiesReq, ): Promise { return this.rpcRequest({ method: 'package.properties', params }) } async getPackageLogs( params: RR.GetPackageLogsReq, ): Promise { return this.rpcRequest({ method: 'package.logs', params }) } async followPackageLogs( params: RR.FollowServerLogsReq, ): Promise { return this.rpcRequest({ method: 'package.logs.follow', params }) } async installPackage( params: RR.InstallPackageReq, ): Promise { return this.rpcRequest({ method: 'package.install', params }) } async getPackageConfig( params: RR.GetPackageConfigReq, ): Promise { return this.rpcRequest({ method: 'package.config.get', params }) } async drySetPackageConfig( params: RR.DrySetPackageConfigReq, ): Promise { return this.rpcRequest({ method: 'package.config.set.dry', params }) } async setPackageConfig( params: RR.SetPackageConfigReq, ): Promise { return this.rpcRequest({ method: 'package.config.set', params }) } async restorePackages( params: RR.RestorePackagesReq, ): Promise { return this.rpcRequest({ method: 'package.backup.restore', params }) } async executePackageAction( params: RR.ExecutePackageActionReq, ): Promise { return this.rpcRequest({ method: 'package.action', params }) } async startPackage(params: RR.StartPackageReq): Promise { return this.rpcRequest({ method: 'package.start', params }) } async restartPackage( params: RR.RestartPackageReq, ): Promise { return this.rpcRequest({ method: 'package.restart', params }) } async stopPackage(params: RR.StopPackageReq): Promise { return this.rpcRequest({ method: 'package.stop', params }) } async uninstallPackage( params: RR.UninstallPackageReq, ): Promise { return this.rpcRequest({ method: 'package.uninstall', params }) } async dryConfigureDependency( params: RR.DryConfigureDependencyReq, ): Promise { return this.rpcRequest({ method: 'package.dependency.configure.dry', params, }) } async sideloadPackage( params: RR.SideloadPackageReq, ): Promise { return this.rpcRequest({ method: 'package.sideload', params, }) } async setInterfaceClearnetAddress( params: RR.SetInterfaceClearnetAddressReq, ): Promise { return this.rpcRequest({ method: 'package.interface.set-clearnet', params }) } async setServiceOutboundProxy( params: RR.SetServiceOutboundProxyReq, ): Promise { return this.rpcRequest({ method: 'package.proxy.set-outbound', params }) } async getSetupStatus() { return this.rpcRequest({ method: 'setup.status', params: {}, }) } private openWebsocket(config: WebSocketSubjectConfig): Observable { const { location } = this.document.defaultView! const protocol = location.protocol === 'http:' ? 'ws' : 'wss' const host = location.host config.url = `${protocol}://${host}/ws${config.url}` return webSocket(config) } private async rpcRequest( options: RPCOptions, urlOverride?: string, ): Promise { const res = await this.http.rpcRequest(options, urlOverride) const body = res.body if (isRpcError(body)) { if (body.error.code === 34) { console.error('Unauthenticated, logging out') this.auth.setUnverified() } throw new RpcError(body.error) } const patchSequence = res.headers.get('x-patch-sequence') if (patchSequence) await firstValueFrom( this.patch.cache$.pipe( filter(({ sequence }) => sequence >= Number(patchSequence)), ), ) return body.result } private async httpRequest(opts: HttpOptions): Promise { const res = await this.http.httpRequest(opts) return res.body } }