2021-02-03 19:21:22 +01:00
|
|
|
import { existsSync, promises as fs } from "fs";
|
2021-05-03 09:59:40 +02:00
|
|
|
import { Socket } from "net";
|
2021-02-03 19:21:22 +01:00
|
|
|
import { homedir, userInfo } from "os";
|
2020-10-12 21:18:28 +02:00
|
|
|
import * as path from "path";
|
|
|
|
import * as util from "util";
|
|
|
|
|
2020-10-21 16:48:40 +02:00
|
|
|
import { ipcMain } from "electron";
|
2022-02-24 20:50:19 +01:00
|
|
|
import * as ipc from "node-ipc";
|
|
|
|
|
2023-06-06 22:34:53 +02:00
|
|
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
2022-12-02 12:45:09 +01:00
|
|
|
|
2023-04-11 17:24:42 +02:00
|
|
|
import { getIpcSocketRoot } from "../proxy/ipc";
|
|
|
|
|
2022-12-02 12:45:09 +01:00
|
|
|
import { WindowMain } from "./window.main";
|
2020-10-12 21:18:28 +02:00
|
|
|
|
|
|
|
export class NativeMessagingMain {
|
2021-05-03 09:59:40 +02:00
|
|
|
private connected: Socket[] = [];
|
2020-12-18 15:47:48 +01:00
|
|
|
private socket: any;
|
2021-12-20 15:47:17 +01:00
|
|
|
|
2021-03-25 23:11:31 +01:00
|
|
|
constructor(
|
|
|
|
private logService: LogService,
|
|
|
|
private windowMain: WindowMain,
|
|
|
|
private userPath: string,
|
|
|
|
private exePath: string
|
2021-12-20 15:47:17 +01:00
|
|
|
) {}
|
|
|
|
|
2021-03-18 21:33:14 +01:00
|
|
|
async listen() {
|
2020-10-12 21:18:28 +02:00
|
|
|
ipc.config.id = "bitwarden";
|
|
|
|
ipc.config.retry = 1500;
|
2023-04-11 16:45:24 +02:00
|
|
|
const ipcSocketRoot = getIpcSocketRoot();
|
|
|
|
if (ipcSocketRoot != null) {
|
|
|
|
ipc.config.socketRoot = ipcSocketRoot;
|
2021-12-20 15:47:17 +01:00
|
|
|
}
|
2020-10-12 21:18:28 +02:00
|
|
|
|
2021-03-25 23:11:31 +01:00
|
|
|
ipc.serve(() => {
|
|
|
|
ipc.server.on("message", (data: any, socket: any) => {
|
|
|
|
this.socket = socket;
|
|
|
|
this.windowMain.win.webContents.send("nativeMessaging", data);
|
2021-12-20 15:47:17 +01:00
|
|
|
});
|
2020-10-12 21:18:28 +02:00
|
|
|
|
2021-01-26 19:11:36 +01:00
|
|
|
ipcMain.on("nativeMessagingReply", (event, msg) => {
|
2021-03-18 21:33:14 +01:00
|
|
|
if (this.socket != null && msg != null) {
|
2021-01-26 22:32:25 +01:00
|
|
|
this.send(msg, this.socket);
|
2021-01-26 19:11:36 +01:00
|
|
|
}
|
2021-12-20 15:47:17 +01:00
|
|
|
});
|
2020-10-12 21:18:28 +02:00
|
|
|
|
2021-05-03 09:59:40 +02:00
|
|
|
ipc.server.on("connect", (socket: Socket) => {
|
|
|
|
this.connected.push(socket);
|
2020-10-12 21:18:28 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
ipc.server.on("socket.disconnected", (socket, destroyedSocketID) => {
|
2021-05-03 09:59:40 +02:00
|
|
|
const index = this.connected.indexOf(socket);
|
|
|
|
if (index > -1) {
|
2020-10-12 21:18:28 +02:00
|
|
|
this.connected.splice(index, 1);
|
|
|
|
}
|
|
|
|
|
2020-12-18 15:47:48 +01:00
|
|
|
this.socket = null;
|
2020-10-12 21:18:28 +02:00
|
|
|
ipc.log("client " + destroyedSocketID + " has disconnected!");
|
2021-12-20 15:47:17 +01:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2020-10-12 21:18:28 +02:00
|
|
|
ipc.server.start();
|
2021-12-20 15:47:17 +01:00
|
|
|
}
|
|
|
|
|
2020-10-12 21:18:28 +02:00
|
|
|
stop() {
|
|
|
|
ipc.server.stop();
|
2021-05-03 09:59:40 +02:00
|
|
|
// Kill all existing connections
|
|
|
|
this.connected.forEach((socket) => {
|
|
|
|
if (!socket.destroyed) {
|
|
|
|
socket.destroy();
|
2021-12-20 15:47:17 +01:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-10-12 21:18:28 +02:00
|
|
|
send(message: object, socket: any) {
|
|
|
|
ipc.server.emit(socket, "message", message);
|
2021-12-20 15:47:17 +01:00
|
|
|
}
|
|
|
|
|
2020-10-12 21:18:28 +02:00
|
|
|
generateManifests() {
|
|
|
|
const baseJson = {
|
|
|
|
name: "com.8bit.bitwarden",
|
|
|
|
description: "Bitwarden desktop <-> browser bridge",
|
2020-10-21 20:40:24 +02:00
|
|
|
path: this.binaryPath(),
|
2021-12-20 15:47:17 +01:00
|
|
|
type: "stdio",
|
|
|
|
};
|
|
|
|
|
2020-11-25 15:25:18 +01:00
|
|
|
const firefoxJson = {
|
|
|
|
...baseJson,
|
|
|
|
...{ allowed_extensions: ["{446900e4-71c2-419f-a6a7-df9c091e268b}"] },
|
2021-12-20 15:47:17 +01:00
|
|
|
};
|
2020-11-25 15:25:18 +01:00
|
|
|
const chromeJson = {
|
|
|
|
...baseJson,
|
2021-12-20 15:47:17 +01:00
|
|
|
...{
|
2020-11-25 15:25:18 +01:00
|
|
|
allowed_origins: [
|
|
|
|
"chrome-extension://nngceckbapebfimnlniiiahkandclblb/",
|
|
|
|
"chrome-extension://jbkfoedolllekgbhcbcoahefnbanhhlh/",
|
2021-02-08 19:58:44 +01:00
|
|
|
"chrome-extension://ccnckbpmaceehanjmeomladnmlffdjgn/",
|
2021-12-20 15:47:17 +01:00
|
|
|
],
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2020-10-12 21:18:28 +02:00
|
|
|
switch (process.platform) {
|
2022-02-24 20:50:19 +01:00
|
|
|
case "win32": {
|
2020-10-21 16:48:40 +02:00
|
|
|
const destination = path.join(this.userPath, "browsers");
|
2020-10-12 21:18:28 +02:00
|
|
|
this.writeManifest(path.join(destination, "firefox.json"), firefoxJson);
|
|
|
|
this.writeManifest(path.join(destination, "chrome.json"), chromeJson);
|
2021-12-20 15:47:17 +01:00
|
|
|
|
2021-01-04 20:31:33 +01:00
|
|
|
this.createWindowsRegistry(
|
|
|
|
"HKLM\\SOFTWARE\\Mozilla\\Firefox",
|
|
|
|
"HKCU\\SOFTWARE\\Mozilla\\NativeMessagingHosts\\com.8bit.bitwarden",
|
|
|
|
path.join(destination, "firefox.json")
|
2021-12-20 15:47:17 +01:00
|
|
|
);
|
2021-01-04 20:31:33 +01:00
|
|
|
this.createWindowsRegistry(
|
|
|
|
"HKCU\\SOFTWARE\\Google\\Chrome",
|
|
|
|
"HKCU\\SOFTWARE\\Google\\Chrome\\NativeMessagingHosts\\com.8bit.bitwarden",
|
|
|
|
path.join(destination, "chrome.json")
|
2021-12-20 15:47:17 +01:00
|
|
|
);
|
|
|
|
break;
|
2022-02-24 20:50:19 +01:00
|
|
|
}
|
|
|
|
case "darwin": {
|
2021-03-15 05:11:56 +01:00
|
|
|
const nmhs = this.getDarwinNMHS();
|
|
|
|
for (const [key, value] of Object.entries(nmhs)) {
|
|
|
|
if (existsSync(value)) {
|
2021-03-23 23:35:25 +01:00
|
|
|
const p = path.join(value, "NativeMessagingHosts", "com.8bit.bitwarden.json");
|
2021-12-20 15:47:17 +01:00
|
|
|
|
2021-03-23 23:35:25 +01:00
|
|
|
let manifest: any = chromeJson;
|
2021-03-15 05:11:56 +01:00
|
|
|
if (key === "Firefox") {
|
2021-03-23 23:35:25 +01:00
|
|
|
manifest = firefoxJson;
|
2021-05-03 09:59:40 +02:00
|
|
|
}
|
2020-10-12 21:18:28 +02:00
|
|
|
|
|
|
|
this.writeManifest(p, manifest).catch((e) =>
|
|
|
|
this.logService.error(`Error writing manifest for ${key}. ${e}`)
|
2021-02-03 19:21:22 +01:00
|
|
|
);
|
2020-11-25 15:25:18 +01:00
|
|
|
} else {
|
2021-01-04 20:31:33 +01:00
|
|
|
this.logService.warning(`${key} not found skipping.`);
|
2021-12-20 15:47:17 +01:00
|
|
|
}
|
|
|
|
}
|
2020-10-12 21:18:28 +02:00
|
|
|
break;
|
2022-02-24 20:50:19 +01:00
|
|
|
}
|
2020-10-12 21:18:28 +02:00
|
|
|
case "linux":
|
2021-01-26 18:01:46 +01:00
|
|
|
if (existsSync(`${this.homedir()}/.mozilla/`)) {
|
|
|
|
this.writeManifest(
|
|
|
|
`${this.homedir()}/.mozilla/native-messaging-hosts/com.8bit.bitwarden.json`,
|
|
|
|
firefoxJson
|
2021-03-09 02:49:20 +01:00
|
|
|
);
|
2020-10-12 21:18:28 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (existsSync(`${this.homedir()}/.config/google-chrome/`)) {
|
2021-01-26 18:01:46 +01:00
|
|
|
this.writeManifest(
|
|
|
|
`${this.homedir()}/.config/google-chrome/NativeMessagingHosts/com.8bit.bitwarden.json`,
|
|
|
|
chromeJson
|
2021-03-09 02:49:20 +01:00
|
|
|
);
|
2020-10-12 21:18:28 +02:00
|
|
|
}
|
|
|
|
|
2021-03-23 23:35:25 +01:00
|
|
|
if (existsSync(`${this.homedir()}/.config/microsoft-edge/`)) {
|
|
|
|
this.writeManifest(
|
|
|
|
`${this.homedir()}/.config/microsoft-edge/NativeMessagingHosts/com.8bit.bitwarden.json`,
|
|
|
|
chromeJson
|
2021-03-15 05:11:56 +01:00
|
|
|
);
|
|
|
|
}
|
2021-12-20 15:47:17 +01:00
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
2021-03-15 05:11:56 +01:00
|
|
|
}
|
2021-12-20 15:47:17 +01:00
|
|
|
}
|
|
|
|
|
2022-09-23 21:47:17 +02:00
|
|
|
generateDdgManifests() {
|
|
|
|
const manifest = {
|
|
|
|
name: "com.8bit.bitwarden",
|
|
|
|
description: "Bitwarden desktop <-> DuckDuckGo bridge",
|
|
|
|
path: this.binaryPath(),
|
|
|
|
type: "stdio",
|
|
|
|
};
|
|
|
|
switch (process.platform) {
|
|
|
|
case "darwin": {
|
|
|
|
/* eslint-disable-next-line no-useless-escape */
|
|
|
|
const path = `${this.homedir()}/Library/Containers/com.duckduckgo.macos.browser/Data/Library/Application\ Support/NativeMessagingHosts/com.8bit.bitwarden.json`;
|
|
|
|
this.writeManifest(path, manifest).catch((e) =>
|
|
|
|
this.logService.error(`Error writing manifest for DuckDuckGo. ${e}`)
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-12 21:18:28 +02:00
|
|
|
removeManifests() {
|
|
|
|
switch (process.platform) {
|
|
|
|
case "win32":
|
2020-10-21 16:48:40 +02:00
|
|
|
fs.unlink(path.join(this.userPath, "browsers", "firefox.json"));
|
|
|
|
fs.unlink(path.join(this.userPath, "browsers", "chrome.json"));
|
2020-10-12 21:18:28 +02:00
|
|
|
this.deleteWindowsRegistry(
|
|
|
|
"HKCU\\SOFTWARE\\Mozilla\\NativeMessagingHosts\\com.8bit.bitwarden"
|
2021-12-20 15:47:17 +01:00
|
|
|
);
|
2020-10-12 21:18:28 +02:00
|
|
|
this.deleteWindowsRegistry(
|
2021-01-04 20:31:33 +01:00
|
|
|
"HKCU\\SOFTWARE\\Google\\Chrome\\NativeMessagingHosts\\com.8bit.bitwarden"
|
2021-12-20 15:47:17 +01:00
|
|
|
);
|
|
|
|
break;
|
2022-02-24 20:50:19 +01:00
|
|
|
case "darwin": {
|
2021-03-15 05:11:56 +01:00
|
|
|
const nmhs = this.getDarwinNMHS();
|
2022-02-24 20:50:19 +01:00
|
|
|
for (const [, value] of Object.entries(nmhs)) {
|
2021-03-23 23:35:25 +01:00
|
|
|
const p = path.join(value, "NativeMessagingHosts", "com.8bit.bitwarden.json");
|
2021-03-15 05:11:56 +01:00
|
|
|
if (existsSync(p)) {
|
|
|
|
fs.unlink(p);
|
2021-12-20 15:47:17 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
2022-02-24 20:50:19 +01:00
|
|
|
}
|
2020-10-12 21:18:28 +02:00
|
|
|
case "linux":
|
2021-12-20 15:47:17 +01:00
|
|
|
if (
|
2021-01-26 18:01:46 +01:00
|
|
|
existsSync(`${this.homedir()}/.mozilla/native-messaging-hosts/com.8bit.bitwarden.json`)
|
2021-12-20 15:47:17 +01:00
|
|
|
) {
|
2021-01-26 18:01:46 +01:00
|
|
|
fs.unlink(`${this.homedir()}/.mozilla/native-messaging-hosts/com.8bit.bitwarden.json`);
|
2021-12-20 15:47:17 +01:00
|
|
|
}
|
2021-03-15 05:11:56 +01:00
|
|
|
|
2021-03-18 21:33:14 +01:00
|
|
|
if (
|
|
|
|
existsSync(
|
|
|
|
`${this.homedir()}/.config/google-chrome/NativeMessagingHosts/com.8bit.bitwarden.json`
|
2021-12-20 15:47:17 +01:00
|
|
|
)
|
2021-03-18 21:33:14 +01:00
|
|
|
) {
|
|
|
|
fs.unlink(
|
|
|
|
`${this.homedir()}/.config/google-chrome/NativeMessagingHosts/com.8bit.bitwarden.json`
|
|
|
|
);
|
|
|
|
}
|
2020-10-12 21:18:28 +02:00
|
|
|
|
2021-12-20 15:47:17 +01:00
|
|
|
if (
|
2021-03-09 02:49:20 +01:00
|
|
|
existsSync(
|
2020-10-21 20:40:24 +02:00
|
|
|
`${this.homedir()}/.config/microsoft-edge/NativeMessagingHosts/com.8bit.bitwarden.json`
|
|
|
|
)
|
2020-10-12 21:26:26 +02:00
|
|
|
) {
|
|
|
|
fs.unlink(
|
2021-05-03 09:59:40 +02:00
|
|
|
`${this.homedir()}/.config/microsoft-edge/NativeMessagingHosts/com.8bit.bitwarden.json`
|
|
|
|
);
|
2020-10-12 21:18:28 +02:00
|
|
|
}
|
2021-12-20 15:47:17 +01:00
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-23 21:47:17 +02:00
|
|
|
removeDdgManifests() {
|
|
|
|
switch (process.platform) {
|
|
|
|
case "darwin": {
|
|
|
|
/* eslint-disable-next-line no-useless-escape */
|
|
|
|
const path = `${this.homedir()}/Library/Containers/com.duckduckgo.macos.browser/Data/Library/Application\ Support/NativeMessagingHosts/com.8bit.bitwarden.json`;
|
|
|
|
if (existsSync(path)) {
|
|
|
|
fs.unlink(path);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-15 05:11:56 +01:00
|
|
|
private getDarwinNMHS() {
|
2022-02-24 20:50:19 +01:00
|
|
|
/* eslint-disable no-useless-escape */
|
2021-12-20 15:47:17 +01:00
|
|
|
return {
|
2021-03-23 23:35:25 +01:00
|
|
|
Firefox: `${this.homedir()}/Library/Application\ Support/Mozilla/`,
|
|
|
|
Chrome: `${this.homedir()}/Library/Application\ Support/Google/Chrome/`,
|
|
|
|
"Chrome Beta": `${this.homedir()}/Library/Application\ Support/Google/Chrome\ Beta/`,
|
|
|
|
"Chrome Dev": `${this.homedir()}/Library/Application\ Support/Google/Chrome\ Dev/`,
|
|
|
|
"Chrome Canary": `${this.homedir()}/Library/Application\ Support/Google/Chrome\ Canary/`,
|
2021-04-29 16:12:57 +02:00
|
|
|
Chromium: `${this.homedir()}/Library/Application\ Support/Chromium/`,
|
2021-03-23 23:35:25 +01:00
|
|
|
"Microsoft Edge": `${this.homedir()}/Library/Application\ Support/Microsoft\ Edge/`,
|
|
|
|
"Microsoft Edge Beta": `${this.homedir()}/Library/Application\ Support/Microsoft\ Edge\ Beta/`,
|
|
|
|
"Microsoft Edge Dev": `${this.homedir()}/Library/Application\ Support/Microsoft\ Edge\ Dev/`,
|
|
|
|
"Microsoft Edge Canary": `${this.homedir()}/Library/Application\ Support/Microsoft\ Edge\ Canary/`,
|
|
|
|
Vivaldi: `${this.homedir()}/Library/Application\ Support/Vivaldi/`,
|
2021-12-20 15:47:17 +01:00
|
|
|
};
|
2022-02-24 20:50:19 +01:00
|
|
|
/* eslint-enable no-useless-escape */
|
2021-12-20 15:47:17 +01:00
|
|
|
}
|
|
|
|
|
2021-03-18 21:33:14 +01:00
|
|
|
private async writeManifest(destination: string, manifest: object) {
|
2021-03-09 02:49:20 +01:00
|
|
|
if (!existsSync(path.dirname(destination))) {
|
2021-01-04 20:31:33 +01:00
|
|
|
await fs.mkdir(path.dirname(destination));
|
2021-12-20 15:47:17 +01:00
|
|
|
}
|
2020-10-12 21:18:28 +02:00
|
|
|
fs.writeFile(destination, JSON.stringify(manifest, null, 2)).catch(this.logService.error);
|
2021-12-20 15:47:17 +01:00
|
|
|
}
|
2020-10-12 21:26:26 +02:00
|
|
|
|
2020-10-12 21:18:28 +02:00
|
|
|
private binaryPath() {
|
2021-01-26 18:01:46 +01:00
|
|
|
if (process.platform === "win32") {
|
2021-03-25 23:11:31 +01:00
|
|
|
return path.join(path.dirname(this.exePath), "resources", "native-messaging.bat");
|
2020-10-12 21:18:28 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return this.exePath;
|
2021-12-20 15:47:17 +01:00
|
|
|
}
|
2020-10-12 21:18:28 +02:00
|
|
|
|
2022-02-03 19:39:23 +01:00
|
|
|
private getRegeditInstance() {
|
2022-02-24 20:50:19 +01:00
|
|
|
// eslint-disable-next-line
|
2020-10-12 21:18:28 +02:00
|
|
|
const regedit = require("regedit");
|
2022-02-03 19:39:23 +01:00
|
|
|
regedit.setExternalVBSLocation(path.join(path.dirname(this.exePath), "resources/regedit/vbs"));
|
|
|
|
|
|
|
|
return regedit;
|
|
|
|
}
|
|
|
|
|
|
|
|
private async createWindowsRegistry(check: string, location: string, jsonFile: string) {
|
|
|
|
const regedit = this.getRegeditInstance();
|
2020-10-12 21:18:28 +02:00
|
|
|
|
2021-02-03 19:21:22 +01:00
|
|
|
const list = util.promisify(regedit.list);
|
|
|
|
const createKey = util.promisify(regedit.createKey);
|
2020-10-12 21:18:28 +02:00
|
|
|
const putValue = util.promisify(regedit.putValue);
|
|
|
|
|
2021-01-04 20:31:33 +01:00
|
|
|
this.logService.debug(`Adding registry: ${location}`);
|
2020-10-12 21:18:28 +02:00
|
|
|
|
|
|
|
// Check installed
|
|
|
|
try {
|
2021-02-03 19:21:22 +01:00
|
|
|
await list(check);
|
2020-10-12 21:18:28 +02:00
|
|
|
} catch {
|
2021-03-15 05:11:56 +01:00
|
|
|
this.logService.warning(`Not finding registry ${check} skipping.`);
|
2021-12-20 15:47:17 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
2020-10-12 21:18:28 +02:00
|
|
|
await createKey(location);
|
2021-12-20 15:47:17 +01:00
|
|
|
|
2020-10-12 21:18:28 +02:00
|
|
|
// Insert path to manifest
|
|
|
|
const obj: any = {};
|
|
|
|
obj[location] = {
|
|
|
|
default: {
|
|
|
|
value: jsonFile,
|
|
|
|
type: "REG_DEFAULT",
|
|
|
|
},
|
2021-02-03 19:21:22 +01:00
|
|
|
};
|
2021-12-20 15:47:17 +01:00
|
|
|
|
2020-10-12 21:18:28 +02:00
|
|
|
return putValue(obj);
|
|
|
|
} catch (error) {
|
|
|
|
this.logService.error(error);
|
|
|
|
}
|
2021-12-20 15:47:17 +01:00
|
|
|
}
|
2020-10-12 21:18:28 +02:00
|
|
|
|
|
|
|
private async deleteWindowsRegistry(key: string) {
|
2022-02-03 19:39:23 +01:00
|
|
|
const regedit = this.getRegeditInstance();
|
2020-10-12 21:18:28 +02:00
|
|
|
|
|
|
|
const list = util.promisify(regedit.list);
|
|
|
|
const deleteKey = util.promisify(regedit.deleteKey);
|
|
|
|
|
2021-02-03 19:21:22 +01:00
|
|
|
this.logService.debug(`Removing registry: ${key}`);
|
2020-10-12 21:18:28 +02:00
|
|
|
|
|
|
|
try {
|
|
|
|
await list(key);
|
|
|
|
await deleteKey(key);
|
|
|
|
} catch {
|
2021-10-21 11:10:36 +02:00
|
|
|
this.logService.error(`Unable to delete registry key: ${key}`);
|
2020-10-12 21:18:28 +02:00
|
|
|
}
|
2021-12-20 15:47:17 +01:00
|
|
|
}
|
2021-01-26 18:01:46 +01:00
|
|
|
|
|
|
|
private homedir() {
|
|
|
|
if (process.platform === "darwin") {
|
|
|
|
return userInfo().homedir;
|
|
|
|
} else {
|
|
|
|
return homedir();
|
|
|
|
}
|
2021-12-20 15:47:17 +01:00
|
|
|
}
|
2020-10-12 21:18:28 +02:00
|
|
|
}
|