Stan 1 сар өмнө
commit
ea56b167d7

+ 24 - 0
Dockerfile.alpine

@@ -0,0 +1,24 @@
+FROM alpine:latest
+ENV MOZ_ENABLE_WAYLAND=1
+RUN apk add -U \
+    firefox \
+    ttf-dejavu \
+    ttf-liberation \
+    py3-pip
+RUN adduser user --disabled-password && \
+    mkdir -p /home/user && \
+    chown user:user /home/user && \
+    chmod 0755 /home/user
+USER user
+WORKDIR /app
+COPY --chown=user addons/ /app/addons/
+COPY --chown=user profiles/ /app/profiles/
+COPY --chown=user main.py /app/main.py
+COPY --chown=user requirements.txt /app/requirements.txt
+RUN python3 -m venv ~/venv && \
+    . ~/venv/bin/activate && \
+    pip3 install -r requirements.txt
+RUN . ~/venv/bin/activate && \
+    python3 main.py --configs arzamas bryansk voronezsh
+RUN rm -rf /app/*
+ENTRYPOINT [ "/usr/bin/firefox" ]

+ 34 - 0
Dockerfile.arch

@@ -0,0 +1,34 @@
+# Base
+FROM python:3.11.13-alpine3.22
+ENV MOZ_ENABLE_WAYLAND=1
+RUN apk add -U \
+    firefox \
+    font-dejavu \
+    font-liberation \
+    py3-pip
+RUN adduser user --disabled-password && \
+    mkdir -p /home/user && \
+    chown user:user /home/user && \
+    chmod 0755 /home/user
+
+USER user
+WORKDIR /app
+
+# Requirements
+COPY --chown=user requirements.txt /app/requirements.txt
+RUN python3 -m venv ~/venv && \
+    . ~/venv/bin/activate && \
+    pip3 install -r requirements.txt
+
+# Profiles
+COPY --chown=user addons/ /app/addons/
+COPY --chown=user profiles/ /app/profiles/
+COPY --chown=user main.py /app/main.py
+RUN . ~/venv/bin/activate && \
+    python3 main.py --configs arzamas bryansk voronezsh
+
+# Cleanup
+RUN rm -rf /app/*
+
+# Entrypoint
+ENTRYPOINT [ "/usr/bin/firefox" ]

+ 44 - 0
Makefile

@@ -0,0 +1,44 @@
+extension.download:
+	wget -O \
+		addons/easyscreenshot-3.109.xpi \
+		https://addons.mozilla.org/firefox/downloads/file/4049242/easyscreenshot-3.109.xpi
+
+	wget -O \
+		addons/image_block-5.1.xpi \
+		https://addons.mozilla.org/firefox/downloads/file/4270226/image_block-5.1resigned1.xpi
+
+	wget -O \
+		addons/multi_account_containers-8.1.3.xpi \
+		https://addons.mozilla.org/firefox/downloads/file/4186050/multi_account_containers-8.1.3.xpi
+
+	wget -O \
+		addons/noscript-11.4.29.xpi \
+		https://addons.mozilla.org/firefox/downloads/file/4206186/noscript-11.4.29.xpi
+
+	wget -O \
+		addons/save_all_images_webextension-0.8.1.xpi \
+		https://addons.mozilla.org/firefox/downloads/file/4227735/save_all_images_webextension-0.8.1.xpi
+
+	wget -O \
+		addons/startpage_private_search-2.0.1.xpi \
+		https://addons.mozilla.org/firefox/downloads/file/4204954/startpage_private_search-2.0.1.xpi
+
+	wget -O \
+		addons/tineye_reverse_image_search-2.0.9.xpi \
+		https://addons.mozilla.org/firefox/downloads/file/4452436/tineye_reverse_image_search-2.0.9.xpi
+
+image.build: version ?= latest
+image.build:
+	docker buildx build \
+		--network host \
+		--platform linux/amd64 \
+		--file Dockerfile.arch \
+		--tag registry.buran.team/infrastructure/application/firefox-amd64:${version} \
+		.
+
+# docker buildx build \
+# 	--network host \
+# 	--platform linux/arm/v7 \
+# 	--file Dockerfile.alpine \
+# 	--tag registry.buran.team/infrastructure/application/firefox-armv7l:${version} \
+# 	.

+ 2 - 0
addons/.gitignore

@@ -0,0 +1,2 @@
+*
+!.gitignore

+ 153 - 0
main.py

@@ -0,0 +1,153 @@
+import os
+import configparser
+import shutil
+
+from typing import Optional
+from time import sleep
+from argparse import ArgumentParser
+from subprocess import run, Popen
+from sys import stderr, stdout
+from yaml import load, SafeLoader
+
+from marionette_driver.addons import Addons
+from marionette_driver.marionette import Marionette
+from marionette_driver.geckoinstance import DesktopInstance
+from mozprofile import FirefoxProfile
+
+
+def shell_execute(cmd: str, env: Optional[dict] = None):
+    if env is None:
+        env = {}
+
+    run(cmd, env=env, stdout=stdout, stderr=stderr, shell=True)
+
+
+def profile_list() -> list:
+    result = []
+
+    ini = configparser.ConfigParser()
+    ini.read(os.path.expanduser("~/.mozilla/firefox/profiles.ini"))
+    for _, values in ini.items():
+        for key, value in values.items():
+            if key != "path":
+                continue
+
+            result.append(value)
+
+    return list(set(result))
+
+
+def profile_create(name_1: str):
+    shell_execute(f"/usr/bin/firefox --createprofile {name_1} -headless")
+
+
+def profile_configure(name_1: str, name_2: str):
+    cwd = os.path.dirname(os.path.realpath(__file__))
+
+    # Read config
+    config = {}
+    with open(f"{cwd}/profiles/{name_1}.yaml", "rt", encoding="utf-8") as handle:
+        config = load(handle, Loader=SafeLoader)
+
+    # Install signed addons first
+    # Start firefox
+    profile = FirefoxProfile(name_1)
+    firefox = DesktopInstance(
+        host="127.0.0.1",
+        port=2828,
+        bin="/usr/bin/firefox",
+        app_args=["-remote-allow-system-access"],
+        profile=profile,
+        headless=1,
+    )
+    firefox.start()
+
+    # Init marionette
+    marionette = Marionette()
+    marionette.start_session()
+
+    # Install addons
+    addons = Addons(marionette)
+    for addon in config["addons"]:
+        addons.install(f"{cwd}/addons/{addon['id']}-{addon['version']}.xpi")
+
+    firefox.close()
+
+    # Set prefs
+    with open(f"{cwd}/{name_1}/user.js", "wt", encoding="utf-8") as handle:
+        for key, value in config["prefs"].items():
+            if type(value) == str:
+                value = f'"{value}"'
+            elif type(value) == bool:
+                value = 'true' if value else 'false'
+            else:
+                value = value
+
+            handle.write(f'user_pref("{key}", {value});\n')
+
+    # Install profile
+    from_path = f"{cwd}/{name_1}"
+    to_path = os.path.expanduser(f"~/.mozilla/firefox/{name_2}")
+
+    if os.path.exists(to_path):
+        shutil.rmtree(to_path)
+
+    shutil.copytree(from_path, to_path, dirs_exist_ok=True)
+    shutil.rmtree(from_path)
+
+
+def profile_init(name_1: str):
+    proc = Popen(f"/usr/bin/firefox -p {name_1} -headless", shell=True)
+    sleep(5)  # TODO: investigate why firefox take a time to apply extensions
+    proc.kill()
+
+
+def profile_patch(name_2: str):
+    cwd = os.path.dirname(os.path.realpath(__file__))
+
+    # Patch profile
+    from_path = f"{cwd}/patches/search.json.mozlz4"
+    to_path = os.path.expanduser(f"~/.mozilla/firefox/{name_2}/search.json.mozlz4")
+
+    shutil.copy(from_path, to_path)
+
+
+def main():
+    parser = ArgumentParser()
+    parser.add_argument("--configs", type=str, nargs="*", required=True)
+    args = parser.parse_args()
+
+    try:
+        shutil.rmtree(os.path.expanduser("~/.mozilla"))
+    except:
+        pass
+
+    names_1 = []
+
+    for config in args.configs:
+        names_1.append(config)
+
+    for name_1 in names_1:
+        profile_create(name_1)
+        sleep(1)  # Prevent profile hash collision
+
+    names_2 = profile_list()
+    mapping = {}
+
+    for name_1 in names_1:
+        for name_2 in names_2:
+            if name_1 in name_2:
+                mapping[name_1] = name_2
+                break
+
+    for name_1, name_2 in mapping.items():
+        profile_configure(name_1, name_2)
+        profile_init(name_1)  # TODO: remove this workaround
+        # profile_patch(name_2)  # TODO: patching not working for now, dynamic patching needed
+
+
+if __name__ == "__main__":
+    try:
+        main()
+    except Exception as e:
+        print(e)

+ 150 - 0
mozlz4/main.py

@@ -0,0 +1,150 @@
+#!/usr/bin/env python3
+# vim: sw=4 ts=4 et tw=100 cc=+1
+#
+####################################################################################################
+#                                           DESCRIPTION                                            #
+####################################################################################################
+#
+# Decompressor/compressor for files in Mozilla's "mozLz4" format. Firefox uses this file format to
+# compress e. g. bookmark backups (*.jsonlz4).
+#
+# This file format is in fact just plain LZ4 data with a custom header (magic number [8 bytes] and
+# uncompressed file size [4 bytes, little endian]).
+#
+####################################################################################################
+#                                           DEPENDENCIES                                           #
+####################################################################################################
+#
+# - Tested with Python 3.10
+# - LZ4 bindings for Python, version 4.x: https://pypi.python.org/pypi/lz4
+#
+####################################################################################################
+#                                             LICENSE                                              #
+####################################################################################################
+#
+# Copyright (c) 2015-2022, Tilman Blumenbach
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without modification, are permitted
+# provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this list of conditions
+#    and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright notice, this list of
+#    conditions and the following disclaimer in the documentation and/or other materials provided
+#    with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+# FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import argparse
+import sys
+
+import lz4.block
+
+
+class BinFileArg:
+    def __init__(self, mode):
+        self._mode = mode
+
+    def __call__(self, arg):
+        objs = {
+            "r": sys.stdin.buffer,
+            "w": sys.stdout.buffer,
+        }
+
+        if arg == "-":
+            return objs[self._mode]
+
+        try:
+            return open(arg, self._mode + "b")
+        except OSError as e:
+            raise argparse.ArgumentTypeError(
+                "failed to open file for %s: %s" % (
+                    "reading" if self._mode == "r" else "writing",
+                    e
+                )
+            )
+
+
+def decompress(file_obj):
+    if file_obj.read(8) != b"mozLz40\0":
+        raise ValueError("Invalid magic number")
+
+    return lz4.block.decompress(file_obj.read())
+
+
+def compress(file_obj):
+    compressed = lz4.block.compress(file_obj.read())
+    return b"mozLz40\0" + compressed
+
+
+def get_argparser():
+    p = argparse.ArgumentParser(
+        description="MozLz4a compression/decompression utility"
+    )
+
+    p.add_argument(
+        "-d", "--decompress", "--uncompress",
+        action="store_true",
+        help="Decompress the input file instead of compressing it."
+    )
+
+    p.add_argument(
+        "in_file",
+        type=BinFileArg("r"),
+        help="Path to input file. `-' means standard input."
+    )
+    p.add_argument(
+        "out_file",
+        type=BinFileArg("w"),
+        nargs="?",
+        default="-",
+        help="Path to output file. `-' means standard output (and is the default)."
+    )
+
+    return p
+
+
+def main():
+    args = get_argparser().parse_args()
+
+    try:
+        with args.in_file as fh:
+            if args.decompress:
+                data = decompress(fh)
+            else:
+                data = compress(fh)
+    except Exception as e:
+        print(
+            "Could not compress/decompress file `%s': %s" % (
+                args.in_file.name,
+                e
+            ),
+            file=sys.stderr
+        )
+        sys.exit(4)
+
+    try:
+        with args.out_file as fh:
+            fh.write(data)
+    except Exception as e:
+        print(
+            "Could not write to output file `%s': %s" % (
+                args.out_file.name,
+                e
+            ),
+            file=sys.stderr
+        )
+        sys.exit(5)
+
+
+if __name__ == "__main__":
+    sys.exit(main())
+

+ 1 - 0
mozlz4/requirements.txt

@@ -0,0 +1 @@
+lz4==4.3.3

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
mozlz4/search.json


BIN
mozlz4/search.json.mozlz4


BIN
patches/search.json.mozlz4


+ 57 - 0
profiles/arzamas.yaml

@@ -0,0 +1,57 @@
+addons:
+  - id: "multi_account_containers"
+    version: "8.1.3"
+
+  - id: "noscript"
+    version: "11.4.29"
+
+  - id: "easyscreenshot"
+    version: "3.109"
+
+  - id: "image_block"
+    version: "5.1"
+
+  - id: "tineye_reverse_image_search"
+    version: "2.0.9"
+
+  - id: "startpage_private_search"
+    version: "2.0.1"
+
+#  - id: "bulletproof"
+#    version: "1.0.0"
+
+prefs:
+  "privacy.history.custom": True
+  # Disable XPI signing requirement, works in ESR and Development Edition
+  "xpinstall.signatures.required": false
+  "browser.toolbars.bookmarks.visibility": "always"
+
+  "general.useragent.override": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36"
+
+  "network.proxy.type": 1
+  "network.proxy.socks": "127.0.0.1"
+  "network.proxy.socks_port": 9000
+  "network.proxy.socks_remote_dns": True
+
+css: |
+  /*
+  * Do not remove the @namespace line -- it's required for correct functioning
+  */
+  @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); /* set default namespace to XUL */
+
+  /*
+  * Hide tab bar, navigation bar and scrollbars
+  * !important may be added to force override, but not necessary
+  * #content is not necessary to hide scroll bars
+  */
+
+  #TabsToolbar {
+    visibility: collapse;
+  }
+  #navigator-toolbox {
+    visibility: collapse;
+  }
+  browser {
+    margin-right: -14px;
+    margin-bottom: -14px;
+  }

+ 34 - 0
profiles/bryansk.yaml

@@ -0,0 +1,34 @@
+addons:
+  - id: "multi_account_containers"
+    version: "8.1.3"
+
+  - id: "noscript"
+    version: "11.4.29"
+
+  - id: "easyscreenshot"
+    version: "3.109"
+
+  - id: "image_block"
+    version: "5.1"
+
+  - id: "tineye_reverse_image_search"
+    version: "2.0.9"
+
+  - id: "startpage_private_search"
+    version: "2.0.1"
+
+#  - id: "bulletproof"
+#    version: "1.0.0"
+
+prefs:
+  "privacy.history.custom": True
+  # Disable XPI signing requirement, works in ESR and Development Edition
+  "xpinstall.signatures.required": false
+  "browser.toolbars.bookmarks.visibility": "always"
+
+  "general.useragent.override": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36"
+
+  "network.proxy.type": 1
+  "network.proxy.socks": "127.0.0.1"
+  "network.proxy.socks_port": 9001
+  "network.proxy.socks_remote_dns": True

+ 13 - 0
profiles/voronezsh.yaml

@@ -0,0 +1,13 @@
+addons:
+  - id: "noscript"
+    version: "11.4.29"
+
+  - id: "easyscreenshot"
+    version: "3.109"
+
+prefs:
+  "privacy.history.custom": True
+  "network.proxy.socks": "127.0.0.1"
+  "network.proxy.socks_port": 9000
+  "network.proxy.socks_remote_dns": True
+  "network.proxy.type": 1

+ 12 - 0
requirements.txt

@@ -0,0 +1,12 @@
+PyYAML==6.0.1
+marionette-driver==3.4.0
+mozdevice==4.1.1
+mozfile==3.0.0
+mozinfo==1.2.3
+mozlog==8.0.0
+mozprocess==1.3.1
+mozprofile==2.6.1
+mozrunner==8.3.0
+mozterm==1.0.0
+mozversion==2.4.0
+setuptools

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно