Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nixos/mastodon: sync nginx configuration with upstream's recommendation #198130

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

Izorkin
Copy link
Contributor

@Izorkin Izorkin commented Oct 27, 2022

Description of changes

Update nginx configuration.

cc @erictapen @SuperSandro2000

Things done
  • Built on platform(s)
    • x86_64-linux
    • aarch64-linux
    • x86_64-darwin
    • aarch64-darwin
  • For non-Linux: Is sandbox = true set in nix.conf? (See Nix manual)
  • Tested, as applicable:
  • Tested compilation of all packages that depend on this change using nix-shell -p nixpkgs-review --run "nixpkgs-review rev HEAD". Note: all changes have to be committed, also see nixpkgs-review usage
  • Tested basic functionality of all binary files (usually in ./result/bin/)
  • 22.11 Release Notes (or backporting 22.05 Release notes)
    • (Package updates) Added a release notes entry if the change is major or breaking
    • (Module updates) Added a release notes entry if the change is significant
    • (Module addition) Added a release notes entry if adding a new NixOS module
    • (Release notes changes) Ran nixos/doc/manual/md-to-db.sh to update generated release notes
  • Fits CONTRIBUTING.md.

@erictapen
Copy link
Member

As always: Please explain what you did there and why it is necessary. You are adding 170LOC nginx config and your only comment is "update nginx config". I just don't have time for figuring out where you are going here, you have to tell me.

@Izorkin
Copy link
Contributor Author

Izorkin commented Oct 28, 2022

Changes:

  • Small optimize nginx locations.
  • Added caching for all static files.
  • Changed handles 404 page.
  • Disable regex in locations.
  • Added proxy cache.

Based on this PR - mastodon/mastodon#19438
Split PR into multiple commits?

@erictapen
Copy link
Member

Looks good, I'd wait with review until the upstream PR is merged.

@Izorkin
Copy link
Contributor Author

Izorkin commented Oct 28, 2022

I'd wait with review until the upstream PR is merged.

Guess it won't be soon :(
PR with Brotli compression hanging open for a very long time.

@Izorkin
Copy link
Contributor Author

Izorkin commented Oct 29, 2022

PR merged in upstream

@Izorkin
Copy link
Contributor Author

Izorkin commented Oct 30, 2022

Small fix.

Comment on lines +732 to +947
proxy_buffering off;
proxy_redirect off;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is nginx buffering websockets at all? Also upstream recommends against this unless long polling is used.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find it difficult to answer.
I have been using this configuration on my instance for a long time. I didn't notice any suspicious errors.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be that errors only occur in very specific scenarios and not at all on lightly used instances. In question I'd rather would like to drop this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In upstream the proxy_redirect parameter is disabled:

  location ^~ /api/v1/streaming {
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header Proxy "";

    proxy_pass http://streaming;
    proxy_buffering off;
    proxy_redirect off;
...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me this looks fine, as in it replicates what upstream does? That is the only thing I understand about this tbh, wether it replicates the behaviour recommended by upstream.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I set the same parameters and values as in the upstream.
I don't know much about proxying.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not feeling to great about just copying upstream values especially if we are not sure if they also not just copied that from somewhere without deeper thought.

nixos/modules/services/web-apps/mastodon.nix Outdated Show resolved Hide resolved
nixos/modules/services/web-apps/mastodon.nix Outdated Show resolved Hide resolved
@Izorkin Izorkin force-pushed the update-mastodon-nginx branch 3 times, most recently from cdc2c7c to 01a0c97 Compare October 31, 2022 18:03
@Izorkin
Copy link
Contributor Author

Izorkin commented Oct 31, 2022

Added missing proxy headers.
It seems that all errors have been corrected. Checked on a working instance.

@Izorkin
Copy link
Contributor Author

Izorkin commented Nov 1, 2022

Removed recommendedProxySettingss option as it is no longer required. Also, this option could globally affect other sites.

@Izorkin
Copy link
Contributor Author

Izorkin commented Nov 1, 2022

Added location and settings for sw.js.map file (mastodon ver 4.0.0rc1).

Comment on lines 619 to 865
locations."= /sw.js" = {
tryFiles = "$uri =404";
priority = 2110;

extraConfig = ''
add_header Cache-Control 'public, max-age=604800, must-revalidate';
${nginxCommonHeaders}
'';
};

locations."= /sw.js.map" = {
tryFiles = "$uri =404";
priority = 2120;

extraConfig = ''
add_header Cache-Control 'public, max-age=604800, must-revalidate';
${nginxCommonHeaders}
'';
};

locations."^~ /assets/" = {
tryFiles = "$uri =404";
priority = 2210;

extraConfig = ''
add_header Cache-Control 'public, max-age=2419200, must-revalidate';
${nginxCommonHeaders}
'';
};

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a lot of repetition here. Can you use listToAttrs or so for locations?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate lines have already been moved to the nginxCommonHeaders variable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's still a lot of duplication, particularly the tryFiles and Cache-Control headers, which are the same for each location except sw.js. +1 to turion's suggestion of listToAttrs, especially if priority isn't necessary

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see how it can be done. Are there any examples?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in #202408 i declare sidekiqUnits which calls lib.attrsets.mapAttrs' to transform cfg.sidekiqProcesses (an attrset of attrsets containing jobClasses and threads) into a bunch of systemd units based on a template. I then merge those units with the rest of the config using lib.attrsets.recursiveUpdate.

I think you could take a similar approach here by declaring an attrset of attrsets containing things like the cache-control max-age and the headers to use. you could also have an extraAttrs attr that you merge into the location's attrset and taking precedence over whatever's in the template, if you wanted maximum flexibility for setting location-specific settings, or you could have an extraConfig attr that is simply appended.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will make it harder to read nginx configuration.
I used a similar variant in PeerTube service:

services.nginx = lib.mkIf cfg.configureNginx {
enable = true;
virtualHosts."${cfg.localDomain}" = {
root = "/var/lib/peertube";
# Application
locations."/" = {
tryFiles = "/dev/null @api";
priority = 1110;
};
locations."= /api/v1/videos/upload-resumable" = {
tryFiles = "/dev/null @api";
priority = 1120;
extraConfig = ''
client_max_body_size 0;
proxy_request_buffering off;
'';
};
locations."~ ^/api/v1/videos/(upload|([^/]+/studio/edit))$" = {
tryFiles = "/dev/null @api";
root = cfg.settings.storage.tmp;
priority = 1130;
extraConfig = ''
client_max_body_size 12G;
add_header X-File-Maximum-Size 8G always;
'' + lib.optionalString cfg.enableWebHttps ''
add_header Strict-Transport-Security 'max-age=63072000; includeSubDomains';
'' + lib.optionalString config.services.nginx.virtualHosts.${cfg.localDomain}.http3 ''
add_header Alt-Svc 'h3=":443"; ma=86400';
'';
};
locations."~ ^/api/v1/(videos|video-playlists|video-channels|users/me)" = {
tryFiles = "/dev/null @api";
priority = 1140;
extraConfig = ''
client_max_body_size 6M;
add_header X-File-Maximum-Size 4M always;
'' + lib.optionalString cfg.enableWebHttps ''
add_header Strict-Transport-Security 'max-age=63072000; includeSubDomains';
'' + lib.optionalString config.services.nginx.virtualHosts.${cfg.localDomain}.http3 ''
add_header Alt-Svc 'h3=":443"; ma=86400';
'';
};
locations."@api" = {
proxyPass = "http://127.0.0.1:${toString cfg.listenHttp}";
priority = 1150;
extraConfig = ''
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_connect_timeout 10m;
proxy_send_timeout 10m;
proxy_read_timeout 10m;
client_max_body_size 100k;
send_timeout 10m;
'';
};
# Websocket
locations."/socket.io" = {
tryFiles = "/dev/null @api_websocket";
priority = 1210;
};
locations."/tracker/socket" = {
tryFiles = "/dev/null @api_websocket";
priority = 1220;
extraConfig = ''
proxy_read_timeout 15m;
'';
};
locations."@api_websocket" = {
proxyPass = "http://127.0.0.1:${toString cfg.listenHttp}";
priority = 1230;
extraConfig = ''
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_http_version 1.1;
'';
};
# Bypass PeerTube for performance reasons.
locations."~ ^/client/(assets/images/(icons/icon-36x36\.png|icons/icon-48x48\.png|icons/icon-72x72\.png|icons/icon-96x96\.png|icons/icon-144x144\.png|icons/icon-192x192\.png|icons/icon-512x512\.png|logo\.svg|favicon\.png|default-playlist\.jpg|default-avatar-account\.png|default-avatar-account-48x48\.png|default-avatar-video-channel\.png|default-avatar-video-channel-48x48\.png))$" = {
tryFiles = "/www/client-overrides/$1 /www/client/$1 $1";
priority = 1310;
};
locations."~ ^/client/(.*\.(js|css|png|svg|woff2|otf|ttf|woff|eot))$" = {
alias = "${cfg.package}/client/dist/$1";
priority = 1320;
extraConfig = ''
add_header Cache-Control 'public, max-age=604800, immutable';
'' + lib.optionalString cfg.enableWebHttps ''
add_header Strict-Transport-Security 'max-age=63072000; includeSubDomains';
'' + lib.optionalString config.services.nginx.virtualHosts.${cfg.localDomain}.http3 ''
add_header Alt-Svc 'h3=":443"; ma=86400';
'';
};
locations."~ ^/lazy-static/(avatars|banners)/" = {
tryFiles = "$uri @api";
root = cfg.settings.storage.avatars;
priority = 1330;
extraConfig = ''
if ($request_method = 'OPTIONS') {
${nginxCommonHeaders}
add_header Access-Control-Max-Age 1728000;
add_header Cache-Control 'no-cache';
add_header Content-Type 'text/plain charset=UTF-8';
add_header Content-Length 0;
return 204;
}
${nginxCommonHeaders}
add_header Cache-Control 'public, max-age=7200';
rewrite ^/lazy-static/avatars/(.*)$ /$1 break;
rewrite ^/lazy-static/banners/(.*)$ /$1 break;
'';
};
locations."^~ /lazy-static/previews/" = {
tryFiles = "$uri @api";
root = cfg.settings.storage.previews;
priority = 1340;
extraConfig = ''
if ($request_method = 'OPTIONS') {
${nginxCommonHeaders}
add_header Access-Control-Max-Age 1728000;
add_header Cache-Control 'no-cache';
add_header Content-Type 'text/plain charset=UTF-8';
add_header Content-Length 0;
return 204;
}
${nginxCommonHeaders}
add_header Cache-Control 'public, max-age=7200';
rewrite ^/lazy-static/previews/(.*)$ /$1 break;
'';
};
locations."^~ /static/thumbnails/" = {
tryFiles = "$uri @api";
root = cfg.settings.storage.thumbnails;
priority = 1350;
extraConfig = ''
if ($request_method = 'OPTIONS') {
${nginxCommonHeaders}
add_header Access-Control-Max-Age 1728000;
add_header Cache-Control 'no-cache';
add_header Content-Type 'text/plain charset=UTF-8';
add_header Content-Length 0;
return 204;
}
${nginxCommonHeaders}
add_header Cache-Control 'public, max-age=7200';
rewrite ^/static/thumbnails/(.*)$ /$1 break;
'';
};
locations."^~ /static/redundancy/" = {
tryFiles = "$uri @api";
root = cfg.settings.storage.redundancy;
priority = 1360;
extraConfig = ''
if ($request_method = 'OPTIONS') {
${nginxCommonHeaders}
add_header Access-Control-Max-Age 1728000;
add_header Content-Type 'text/plain charset=UTF-8';
add_header Content-Length 0;
return 204;
}
if ($request_method = 'GET') {
${nginxCommonHeaders}
access_log off;
}
aio threads;
sendfile on;
sendfile_max_chunk 1M;
limit_rate_after 5M;
set $peertube_limit_rate 800k;
set $limit_rate $peertube_limit_rate;
rewrite ^/static/redundancy/(.*)$ /$1 break;
'';
};
locations."^~ /static/streaming-playlists/" = {
tryFiles = "$uri @api";
root = cfg.settings.storage.streaming_playlists;
priority = 1370;
extraConfig = ''
if ($request_method = 'OPTIONS') {
${nginxCommonHeaders}
add_header Access-Control-Max-Age 1728000;
add_header Content-Type 'text/plain charset=UTF-8';
add_header Content-Length 0;
return 204;
}
if ($request_method = 'GET') {
${nginxCommonHeaders}
access_log off;
}
aio threads;
sendfile on;
sendfile_max_chunk 1M;
limit_rate_after 5M;
set $peertube_limit_rate 5M;
set $limit_rate $peertube_limit_rate;
rewrite ^/static/streaming-playlists/(.*)$ /$1 break;
'';
};
locations."~ ^/static/webseed/" = {
tryFiles = "$uri @api";
root = cfg.settings.storage.videos;
priority = 1380;
extraConfig = ''
if ($request_method = 'OPTIONS') {
${nginxCommonHeaders}
add_header Access-Control-Max-Age 1728000;
add_header Content-Type 'text/plain charset=UTF-8';
add_header Content-Length 0;
return 204;
}
if ($request_method = 'GET') {
${nginxCommonHeaders}
access_log off;
}
aio threads;
sendfile on;
sendfile_max_chunk 1M;
limit_rate_after 5M;
set $peertube_limit_rate 800k;
set $limit_rate $peertube_limit_rate;
rewrite ^/static/webseed/(.*)$ /$1 break;
'';
};
extraConfig = lib.optionalString cfg.enableWebHttps ''
add_header Strict-Transport-Security 'max-age=63072000; includeSubDomains';
'';
};
};

@turion
Copy link
Contributor

turion commented Nov 15, 2022

This is a lot of custom config. It is bound to go out of sync with the upstream recommendations eventually. Can we maybe instead simply use the upstream source config file and include it instead of replicating it? https://github.com/mastodon/mastodon/pull/19438/files#diff-47fc0317b265a70f29e18f9c34069ba84a520061e96d0b2dd03c9ee16cfc6b3e

@erictapen
Copy link
Member

erictapen commented Nov 15, 2022

Can we maybe instead simply use the upstream source config file and include it instead of replicating it?

I don't think we have another way than to configure nginx through the NixOS module system, right?
I also very much don't look forward to keep in sync with upstream here, but I don't see an alternative yet.

@turion
Copy link
Contributor

turion commented Nov 15, 2022

I don't think we have another way than to configure nginx through the NixOS module system, right?

Yes, but I would have thought one can use services.nginx.appendHttpConfig or similar to add a whole file? (I'm not an nginx expert though)

@turion
Copy link
Contributor

turion commented Nov 15, 2022

Probably rather services.nginx.virtualHosts.${cfg.localDomain}.extraConfig since we don't want to change other hosts' configs.

@erictapen erictapen mentioned this pull request Nov 15, 2022
13 tasks
@Izorkin
Copy link
Contributor Author

Izorkin commented Nov 15, 2022

Probably rather services.nginx.virtualHosts.${cfg.localDomain}.extraConfig since we don't want to change other hosts' configs.

This is difficult to adapt, also the flexibility of the host configuration will be lost. For example it will not be possible to activate http2/3 protocols.

@Izorkin
Copy link
Contributor Author

Izorkin commented May 7, 2023

Updated and rebased PR.

@Izorkin Izorkin force-pushed the update-mastodon-nginx branch 2 times, most recently from af237ae to a51989b Compare July 4, 2023 08:51
nginxCommonHeaders = ''
add_header Strict-Transport-Security 'max-age=63072000; includeSubDomains';
''
# Mastodon in upstream is not supported HTTP3 protocol.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Mastodon in upstream is not supported HTTP3 protocol.
# Upstream does not supported HTTP3 protocol

Why do we have this, if upstream is not supporting it? Wouldn't we break things then?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.
It won't break. I've been using the HTTP3 protocol on my copy for a long time.

add_header Alt-Svc 'h3=":443"; ma=86400';
'';

nginxProxyHeaders = ''
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we generally consider double proxies. I would just go with recommendedProxyConfig and write a changelog entry that if you are double proxying, you need to change your config.

Comment on lines 809 to 864
locations."= /sw.js" = {
tryFiles = "$uri =404";
priority = 2110;

extraConfig = ''
add_header Cache-Control 'public, max-age=604800, must-revalidate';
${nginxCommonHeaders}
'';
};

locations."= /sw.js.map" = {
tryFiles = "$uri =404";
priority = 2120;

extraConfig = ''
add_header Cache-Control 'public, max-age=604800, must-revalidate';
${nginxCommonHeaders}
'';
};

locations."^~ /assets/" = {
tryFiles = "$uri =404";
priority = 2230;

extraConfig = ''
add_header Cache-Control 'public, max-age=2419200, must-revalidate';
${nginxCommonHeaders}
'';
};

locations."^~ /avatars/" = {
tryFiles = "$uri =404";
priority = 2240;

extraConfig = ''
add_header Cache-Control 'public, max-age=2419200, must-revalidate';
${nginxCommonHeaders}
'';
};

locations."^~ /emoji/" = {
tryFiles = "$uri =404";
priority = 2250;

extraConfig = ''
add_header Cache-Control 'public, max-age=2419200, must-revalidate';
${nginxCommonHeaders}
'';
};

locations."^~ /headers/" = {
tryFiles = "$uri =404";
priority = 2260;

extraConfig = ''
add_header Cache-Control 'public, max-age=2419200, must-revalidate';
${nginxCommonHeaders}
'';
};

locations."^~ /packs/" = {
tryFiles = "$uri =404";
priority = 2270;

extraConfig = ''
add_header Cache-Control 'public, max-age=2419200, must-revalidate';
${nginxCommonHeaders}
'';
};

locations."^~ /sounds/" = {
tryFiles = "$uri =404";
priority = 2280;

extraConfig = ''
add_header Cache-Control 'public, max-age=2419200, must-revalidate';
${nginxCommonHeaders}
'';
};

locations."^~ /system/" = {
tryFiles = "$uri =404";
alias = "/var/lib/mastodon/public-system/";
priority = 2290;

extraConfig = ''
add_header Cache-Control 'public, max-age=2419200, immutable';
${nginxCommonHeaders}
'';
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those all seem to be duplicated except of the priority. I would say we move the common things in to a variable and merge the priority into that.

      let 
        commonLocation = {
          tryFiles = "$uri =404";
          extraConfig = ''
            add_header Cache-Control 'public, max-age=2419200, immutable';
            ${nginxCommonHeaders}
          '';
        };
      in
        locations."^~ /system/" = commonLocatuion / {
          alias = "/var/lib/mastodon/public-system/";
          priority = 2290;
        };

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are several different options, and you have to include variables for each one. It seems to me that this just makes it harder to view configuration.

Comment on lines +732 to +947
proxy_buffering off;
proxy_redirect off;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not feeling to great about just copying upstream values especially if we are not sure if they also not just copied that from somewhere without deeper thought.

@Izorkin Izorkin force-pushed the update-mastodon-nginx branch 2 times, most recently from 0673157 to accd5a4 Compare July 6, 2023 16:25
@Izorkin
Copy link
Contributor Author

Izorkin commented Jul 6, 2023

Add hardened headers to user-uploaded files:

            add_header X-Content-Type-Options 'nosniff';
            add_header Content-Security-Policy "default-src 'none'; form-action 'none'";

@Izorkin
Copy link
Contributor Author

Izorkin commented Oct 21, 2023

Rebased PR.
Update nginx configuration:

diff --git a/nixos/modules/services/web-apps/mastodon.nix b/nixos/modules/services/web-apps/mastodon.nix
index 52a2708bde64..f5756b339396 100644
--- a/nixos/modules/services/web-apps/mastodon.nix
+++ b/nixos/modules/services/web-apps/mastodon.nix
@@ -789,6 +789,9 @@ in {
       upstreams = {
         "backend-mastodon-streaming" = {
           servers = { ${if cfg.enableUnixSocket then "unix:/run/mastodon-streaming/streaming.socket" else "127.0.0.1:${toString cfg.streamingPort}"} = { fail_timeout = "0"; }; };
+          extraConfig = ''
+            least_conn;
+          '';
         };
         "backend-mastodon-web" = {
           servers = { ${if cfg.enableUnixSocket then "unix:/run/mastodon-web/web.socket" else "127.0.0.1:${toString cfg.webPort}"} = { fail_timeout = "0"; }; };

cc @erictapen

@erictapen
Copy link
Member

@Izorkin I incorporated the change for the mastodon-streaming nginx backend into #251950, as I consider it a necessary part of the update.

@Izorkin
Copy link
Contributor Author

Izorkin commented Nov 13, 2023

The rest of it would be good to merge as well.
This configuration is tested on 2 instances.

@Izorkin
Copy link
Contributor Author

Izorkin commented Nov 29, 2023

Rebased and updated PR.

@Izorkin
Copy link
Contributor Author

Izorkin commented Dec 19, 2023

Update nginx configuration.
Now for other static files (500.html, robots.txt, inert.css and etc) located at the site root, the caching parameters are correctly specified.
If there are no static files, the request is now redirected to backend.

@Izorkin Izorkin requested review from SuperSandro2000 and removed request for SuperSandro2000 December 19, 2023 16:15
@Izorkin Izorkin force-pushed the update-mastodon-nginx branch 3 times, most recently from 9281915 to 484beb7 Compare December 24, 2023 22:26
@Izorkin
Copy link
Contributor Author

Izorkin commented Dec 24, 2023

Remove includeSubDomains from Strict-Transport-Security header.

@Izorkin
Copy link
Contributor Author

Izorkin commented Jan 16, 2024

Update nginx locations block.

@Izorkin Izorkin force-pushed the update-mastodon-nginx branch 2 times, most recently from fae33b5 to 9cc7c5e Compare January 28, 2024 15:52
@Izorkin
Copy link
Contributor Author

Izorkin commented Feb 16, 2024

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants