aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--flakes/myuids/flake.nix2
-rw-r--r--modules/private/default.nix2
-rw-r--r--modules/private/environment.nix10
-rw-r--r--modules/private/monitoring/default.nix2
-rw-r--r--modules/private/websites/default.nix1
-rw-r--r--modules/private/websites/ressourcerie_banon/cryptpad.nix115
-rw-r--r--modules/private/websites/tools/cryptpad/default.nix50
-rw-r--r--modules/private/websites/tools/cryptpad/farm.nix180
m---------nixops/secrets0
9 files changed, 256 insertions, 106 deletions
diff --git a/flakes/myuids/flake.nix b/flakes/myuids/flake.nix
index 4068ba3..cc6dc66 100644
--- a/flakes/myuids/flake.nix
+++ b/flakes/myuids/flake.nix
@@ -5,6 +5,7 @@
5 lib = { 5 lib = {
6 # Check that there is no clash with nixos/modules/misc/ids.nix 6 # Check that there is no clash with nixos/modules/misc/ids.nix
7 uids = { 7 uids = {
8 cryptpad = 386;
8 postfixscripts = 387; 9 postfixscripts = 387;
9 acme = 388; 10 acme = 388;
10 backup = 389; 11 backup = 389;
@@ -20,6 +21,7 @@
20 }; 21 };
21 gids = { 22 gids = {
22 nagios = 11; # commented in the ids file 23 nagios = 11; # commented in the ids file
24 cryptpad = 386;
23 acme = 388; 25 acme = 388;
24 backup = 389; 26 backup = 389;
25 vhost = 390; 27 vhost = 390;
diff --git a/modules/private/default.nix b/modules/private/default.nix
index f12fc48..9f99ed9 100644
--- a/modules/private/default.nix
+++ b/modules/private/default.nix
@@ -85,6 +85,8 @@ set = {
85 assetsTools = ./websites/tools/assets; 85 assetsTools = ./websites/tools/assets;
86 cloudTool = ./websites/tools/cloud; 86 cloudTool = ./websites/tools/cloud;
87 cloudFarmTool = ./websites/tools/cloud/farm.nix; 87 cloudFarmTool = ./websites/tools/cloud/farm.nix;
88 cryptpadTool = ./websites/tools/cryptpad;
89 cryptpadFarmTool = ./websites/tools/cryptpad/farm.nix;
88 commentoTool = ./websites/tools/commento; 90 commentoTool = ./websites/tools/commento;
89 davTool = ./websites/tools/dav; 91 davTool = ./websites/tools/dav;
90 vpnTool = ./websites/tools/vpn; 92 vpnTool = ./websites/tools/vpn;
diff --git a/modules/private/environment.nix b/modules/private/environment.nix
index 32af339..980b878 100644
--- a/modules/private/environment.nix
+++ b/modules/private/environment.nix
@@ -1165,6 +1165,16 @@ in
1165 }; 1165 };
1166 }; 1166 };
1167 }; 1167 };
1168 cryptpad = mkOption {
1169 description = "Cryptpad configuration";
1170 type = attrsOf (submodule {
1171 options = {
1172 email = mkOption { type = str; description = "Admin e-mail"; };
1173 admins = mkOption { type = listOf str; description = "Instance admin public keys"; };
1174 port = mkOption { type = port; description = "Port to listen to"; };
1175 };
1176 });
1177 };
1168 ympd = mkOption { 1178 ympd = mkOption {
1169 description = "Ympd configuration"; 1179 description = "Ympd configuration";
1170 type = submodule { 1180 type = submodule {
diff --git a/modules/private/monitoring/default.nix b/modules/private/monitoring/default.nix
index 6c7462f..cab9e7c 100644
--- a/modules/private/monitoring/default.nix
+++ b/modules/private/monitoring/default.nix
@@ -27,7 +27,7 @@ let
27 interface = "eth0"; 27 interface = "eth0";
28 }; 28 };
29 eldiron = { 29 eldiron = {
30 processWarn = "400"; processAlert = "500"; 30 processWarn = "550"; processAlert = "650";
31 loadWarn = "1.0"; loadAlert = "1.2"; 31 loadWarn = "1.0"; loadAlert = "1.2";
32 interface = "eth0"; 32 interface = "eth0";
33 }; 33 };
diff --git a/modules/private/websites/default.nix b/modules/private/websites/default.nix
index b0c7496..07ffc3e 100644
--- a/modules/private/websites/default.nix
+++ b/modules/private/websites/default.nix
@@ -305,6 +305,7 @@ in
305 tools.assets.enable = true; 305 tools.assets.enable = true;
306 tools.cloud.enable = true; 306 tools.cloud.enable = true;
307 tools.commento.enable = true; 307 tools.commento.enable = true;
308 tools.cryptpad.enable = true;
308 tools.dav.enable = true; 309 tools.dav.enable = true;
309 tools.db.enable = true; 310 tools.db.enable = true;
310 tools.diaspora.enable = true; 311 tools.diaspora.enable = true;
diff --git a/modules/private/websites/ressourcerie_banon/cryptpad.nix b/modules/private/websites/ressourcerie_banon/cryptpad.nix
index 09e938b..7aea728 100644
--- a/modules/private/websites/ressourcerie_banon/cryptpad.nix
+++ b/modules/private/websites/ressourcerie_banon/cryptpad.nix
@@ -1,13 +1,14 @@
1{ lib, pkgs, config, ... }: 1{ lib, pkgs, config, ... }:
2let 2let
3 cfg = config.myServices.websites.ressourcerie_banon.cryptpad; 3 cfg = config.myServices.websites.ressourcerie_banon.cryptpad;
4 port = config.myEnv.ports.cryptpad_ressourcerie_banon; 4 envCfg = config.myEnv.tools.cryptpad.ressourcerie_banon;
5 port = envCfg.port;
5 configFile = pkgs.writeText "config.js" '' 6 configFile = pkgs.writeText "config.js" ''
6 // ${pkgs.cryptpad}/lib/node_modules/cryptpad/config/config.example.js 7 // ${pkgs.cryptpad}/lib/node_modules/cryptpad/config/config.example.js
7 module.exports = { 8 module.exports = {
8 httpUnsafeOrigin: 'https://${domain}', 9 httpUnsafeOrigin: 'https://${domain}',
9 httpPort: ${toString port}, 10 httpPort: ${toString port},
10 adminEmail: 'devnull@mail.immae.eu', 11 adminEmail: '${envCfg.email}',
11 filePath: './datastore/', 12 filePath: './datastore/',
12 archivePath: './data/archive', 13 archivePath: './data/archive',
13 pinPath: './data/pins', 14 pinPath: './data/pins',
@@ -21,123 +22,27 @@ let
21 logLevel: 'info', 22 logLevel: 'info',
22 logFeedback: false, 23 logFeedback: false,
23 verbose: false, 24 verbose: false,
25 inactiveTime: false,
26 adminKeys: ${builtins.toJSON envCfg.admins},
24 }; 27 };
25 ''; 28 '';
26 domain = "pad.le-garage-autonome.org"; 29 domain = "pad.le-garage-autonome.org";
27 api_domain = "pad.le-garage-autonome.org";
28 files_domain = "pad.le-garage-autonome.org";
29in { 30in {
30 options.myServices.websites.ressourcerie_banon.cryptpad.enable = lib.mkEnableOption "Enable Ressourcerie Banon’s cryptpad"; 31 options.myServices.websites.ressourcerie_banon.cryptpad.enable = lib.mkEnableOption "Enable Ressourcerie Banon’s cryptpad";
31 32
32 config = lib.mkIf cfg.enable { 33 config = lib.mkIf cfg.enable {
33 systemd.services.cryptpad-ressourcerie_banon = { 34 myServices.tools.cryptpad.farm.hosts.ressourcerie_banon = {
34 description = "Cryptpad Banon Service"; 35 inherit domain port;
35 wantedBy = [ "multi-user.target" ]; 36 config = configFile;
36 after = [ "networking.target" ];
37 serviceConfig = {
38 DynamicUser = true;
39 Environment = [
40 "CRYPTPAD_CONFIG=${configFile}"
41 "HOME=%S/cryptpad/ressourcerie_banon"
42 ];
43 ExecStart = "${pkgs.cryptpad}/bin/cryptpad";
44 PrivateTmp = true;
45 Restart = "always";
46 StateDirectory = "cryptpad/ressourcerie_banon";
47 WorkingDirectory = "%S/cryptpad/ressourcerie_banon";
48 };
49 }; 37 };
50 services.websites.env.production.modules = [ "proxy_wstunnel" ]; 38 services.websites.env.production.modules = [ "proxy_wstunnel" ];
51 services.websites.env.production.vhostConfs.ressourcerie_banon_cryptpad = { 39 services.websites.env.production.vhostConfs.ressourcerie_banon_cryptpad = {
52 certName = "ressourcerie_banon"; 40 certName = "ressourcerie_banon";
53 addToCerts = true; 41 addToCerts = true;
54 hosts = [domain]; 42 hosts = [domain];
55 root = "${pkgs.cryptpad}/lib/node_modules/cryptpad"; 43 root = config.myServices.tools.cryptpad.farm.vhostRoots.ressourcerie_banon;
56 extraConfig = [ 44 extraConfig = [
57 '' 45 config.myServices.tools.cryptpad.farm.vhosts.ressourcerie_banon
58 RewriteEngine On
59
60 Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
61 Header set X-XSS-Protection "1; mode=block"
62 Header set X-Content-Type-Options "nosniff"
63 Header set Access-Control-Allow-Origin "*"
64 Header set Permissions-Policy "interest-cohort=()"
65
66 Header set Cross-Origin-Resource-Policy "cross-origin"
67 <If "%{REQUEST_URI} =~ m#^/(sheet|presentation|doc)/.*$#">
68 Header set Cross-Origin-Opener-Policy "same-origin"
69 </If>
70 Header set Cross-Origin-Embedder-Policy "require-corp"
71
72 ErrorDocument 404 /customize.dist/404.html
73
74 <If "%{QUERY_STRING} =~ m#ver=.*?#">
75 Header set Cache-Control "max-age=31536000"
76 </If>
77 <If "%{REQUEST_URI} =~ m#^/.*(\/|\.html)$#">
78 Header set Cache-Control "no-cache"
79 </If>
80
81 SetEnv styleSrc "'unsafe-inline' 'self' ${domain}"
82 SetEnv connectSrc "'self' https://${domain} ${domain} https://${api_domain} blob: wss://${api_domain} ${api_domain} ${files_domain}"
83 SetEnv fontSrc "'self' data: ${domain}"
84 SetEnv imgSrc "'self' data: * blob: ${domain}"
85 SetEnv frameSrc "'self' blob:"
86 SetEnv mediaSrc "'self' data: * blob: ${domain}"
87 SetEnv childSrc "https://${domain}"
88 SetEnv workerSrc "https://${domain}"
89 SetEnv scriptSrc "'self' resource: ${domain}"
90
91 Header set Content-Security-Policy "default-src 'none'; child-src %{childSrc}e; worker-src %{workerSrc}e; media-src %{mediaSrc}e; style-src %{styleSrc}e; script-src %{scriptSrc}e; connect-src %{connectSrc}e; font-src %{fontSrc}e; img-src %{imgSrc}e; frame-src %{frameSrc}e;"
92
93 RewriteCond %{HTTP:UPGRADE} ^WebSocket$ [NC]
94 RewriteCond %{HTTP:CONNECTION} Upgrade$ [NC]
95 RewriteRule .* ws://localhost:${toString port}%{REQUEST_URI} [P,NE,QSA,L]
96
97 RewriteRule ^/customize/(.*)$ /customize.dist/$1 [L]
98
99 ProxyPassMatch "^/(api/(config|broadcast).*)$" "http://localhost:${toString port}/$1"
100 ProxyPassReverse /api http://localhost:${toString port}/api
101 ProxyPreserveHost On
102 RequestHeader set X-Real-IP %{REMOTE_ADDR}s
103
104 <LocationMatch /blob/>
105 Header set Cache-Control "max-age=31536000"
106 Header set Access-Control-Allow-Origin "*"
107 Header set Access-Control-Allow-Methods "GET, POST, OPTIONS"
108 Header set Access-Control-Allow-Headers "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range,Content-Length"
109 Header set Access-Control-Expose-Headers "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range,Content-Length"
110
111 RewriteCond %{REQUEST_METHOD} OPTIONS
112 RewriteRule ^(.*)$ $1 [R=204,L]
113 </LocationMatch>
114
115 <LocationMatch /block/>
116 Header set Cache-Control "max-age=0"
117 </locationMatch>
118
119 RewriteRule ^/(register|login|settings|user|pad|drive|poll|slide|code|whiteboard|file|media|profile|contacts|todo|filepicker|debug|kanban|sheet|support|admin|notifications|teams|calendar|presentation|doc)$ $1/ [R=302,L]
120
121 RewriteCond %{DOCUMENT_ROOT}/www/%{REQUEST_URI} -f
122 RewriteRule (.*) /www/$1 [L]
123
124 RewriteCond %{DOCUMENT_ROOT}/www/%{REQUEST_URI}/index.html -f
125 RewriteRule (.*) /www/$1/index.html [L]
126
127 RewriteCond %{DOCUMENT_ROOT}/customize.dist/%{REQUEST_URI} -f
128 RewriteRule (.*) /customize.dist/$1 [L]
129
130 <Directory ${pkgs.cryptpad}/lib/node_modules/cryptpad/www>
131 AllowOverride None
132 Require all granted
133 DirectoryIndex index.html
134 </Directory>
135 <Directory ${pkgs.cryptpad}/lib/node_modules/cryptpad/customize.dist>
136 AllowOverride None
137 Require all granted
138 DirectoryIndex index.html
139 </Directory>
140 ''
141 ]; 46 ];
142 }; 47 };
143 }; 48 };
diff --git a/modules/private/websites/tools/cryptpad/default.nix b/modules/private/websites/tools/cryptpad/default.nix
new file mode 100644
index 0000000..69b9877
--- /dev/null
+++ b/modules/private/websites/tools/cryptpad/default.nix
@@ -0,0 +1,50 @@
1{ config, pkgs, lib, ... }:
2let
3 cfg = config.myServices.websites.tools.cryptpad;
4 envCfg = config.myEnv.tools.cryptpad.immaeEu;
5 domain = "cryptpad.immae.eu";
6 port = envCfg.port;
7 configFile = pkgs.writeText "config.js" ''
8 // ${pkgs.cryptpad}/lib/node_modules/cryptpad/config/config.example.js
9 module.exports = {
10 httpUnsafeOrigin: 'https://${domain}',
11 httpPort: ${toString port},
12 adminEmail: '${envCfg.email}',
13 filePath: './datastore/',
14 archivePath: './data/archive',
15 pinPath: './data/pins',
16 taskPath: './data/tasks',
17 blockPath: './block',
18 blobPath: './blob',
19 blobStagingPath: './data/blobstage',
20 decreePath: './data/decrees',
21 logPath: './data/logs',
22 logToStdout: false,
23 logLevel: 'info',
24 logFeedback: false,
25 verbose: false,
26 inactiveTime: false,
27 maxUploadSize: 100 * 1024 * 1024,
28 adminKeys: ${builtins.toJSON envCfg.admins},
29 };
30 '';
31in
32{
33 options.myServices.websites.tools.cryptpad.enable = lib.mkEnableOption "Enable Cryptpad";
34 config = lib.mkIf cfg.enable {
35 myServices.tools.cryptpad.farm.hosts.immaeEu = {
36 inherit domain port;
37 config = configFile;
38 };
39 services.websites.env.tools.modules = [ "proxy_wstunnel" ];
40 services.websites.env.tools.vhostConfs.cryptpad = {
41 certName = "eldiron";
42 addToCerts = true;
43 hosts = [domain];
44 root = config.myServices.tools.cryptpad.farm.vhostRoots.immaeEu;
45 extraConfig = [
46 config.myServices.tools.cryptpad.farm.vhosts.immaeEu
47 ];
48 };
49 };
50}
diff --git a/modules/private/websites/tools/cryptpad/farm.nix b/modules/private/websites/tools/cryptpad/farm.nix
new file mode 100644
index 0000000..b35f348
--- /dev/null
+++ b/modules/private/websites/tools/cryptpad/farm.nix
@@ -0,0 +1,180 @@
1{ pkgs, config, lib, ... }:
2let
3 cfg = config.myServices.tools.cryptpad.farm;
4 toService = name:
5 let
6 inherit (cfg.hosts.${name}) package config;
7 in {
8 description = "Cryptpad ${name} Service";
9 wantedBy = [ "multi-user.target" ];
10 after = [ "networking.target" ];
11 serviceConfig = {
12 User = "cryptpad";
13 Group = "cryptpad";
14 Environment = [
15 "CRYPTPAD_CONFIG=${config}"
16 "HOME=%S/cryptpad/${name}"
17 ];
18 ExecStart = "${package}/bin/cryptpad";
19 PrivateTmp = true;
20 Restart = "always";
21 StateDirectory = "cryptpad/${name}";
22 WorkingDirectory = "%S/cryptpad/${name}";
23 };
24 };
25 toVhostRoot = name: "${cfg.hosts.${name}.package}/lib/node_modules/cryptpad";
26 toVhost = name:
27 let
28 inherit (cfg.hosts.${name}) package domain port;
29 api_domain = domain;
30 files_domain = domain;
31 in ''
32 RewriteEngine On
33
34 Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
35 Header set X-XSS-Protection "1; mode=block"
36 Header set X-Content-Type-Options "nosniff"
37 Header set Access-Control-Allow-Origin "*"
38 Header set Permissions-Policy "interest-cohort=()"
39
40 Header set Cross-Origin-Resource-Policy "cross-origin"
41 <If "%{REQUEST_URI} =~ m#^/(sheet|presentation|doc)/.*$#">
42 Header set Cross-Origin-Opener-Policy "same-origin"
43 </If>
44 Header set Cross-Origin-Embedder-Policy "require-corp"
45
46 ErrorDocument 404 /customize.dist/404.html
47
48 <If "%{QUERY_STRING} =~ m#ver=.*?#">
49 Header set Cache-Control "max-age=31536000"
50 </If>
51 <If "%{REQUEST_URI} =~ m#^/.*(\/|\.html)$#">
52 Header set Cache-Control "no-cache"
53 </If>
54
55 SetEnv styleSrc "'unsafe-inline' 'self' ${domain}"
56 SetEnv connectSrc "'self' https://${domain} ${domain} https://${api_domain} blob: wss://${api_domain} ${api_domain} ${files_domain}"
57 SetEnv fontSrc "'self' data: ${domain}"
58 SetEnv imgSrc "'self' data: * blob: ${domain}"
59 SetEnv frameSrc "'self' blob:"
60 SetEnv mediaSrc "'self' data: * blob: ${domain}"
61 SetEnv childSrc "https://${domain}"
62 SetEnv workerSrc "https://${domain}"
63 SetEnv scriptSrc "'self' 'unsafe-eval' 'unsafe-inline' resource: ${domain}"
64
65 Header set Content-Security-Policy "default-src 'none'; child-src %{childSrc}e; worker-src %{workerSrc}e; media-src %{mediaSrc}e; style-src %{styleSrc}e; script-src %{scriptSrc}e; connect-src %{connectSrc}e; font-src %{fontSrc}e; img-src %{imgSrc}e; frame-src %{frameSrc}e;"
66
67 RewriteCond %{HTTP:UPGRADE} ^WebSocket$ [NC]
68 RewriteCond %{HTTP:CONNECTION} Upgrade$ [NC]
69 RewriteRule .* ws://localhost:${toString port}%{REQUEST_URI} [P,NE,QSA,L]
70
71 RewriteRule ^/customize/(.*)$ /customize.dist/$1 [L]
72
73 ProxyPassMatch "^/(api/(config|broadcast).*)$" "http://localhost:${toString port}/$1"
74 ProxyPassReverse /api http://localhost:${toString port}/api
75 ProxyPreserveHost On
76 RequestHeader set X-Real-IP %{REMOTE_ADDR}s
77
78 Alias /blob /var/lib/cryptpad/${name}/blob
79 <Directory /var/lib/cryptpad/${name}/blob>
80 Require all granted
81 AllowOverride None
82 </Directory>
83 Alias /block /var/lib/cryptpad/${name}/block
84 <Directory /var/lib/cryptpad/${name}/block>
85 Require all granted
86 AllowOverride None
87 </Directory>
88 <LocationMatch /blob/>
89 Header set Cache-Control "max-age=31536000"
90 Header set Access-Control-Allow-Origin "*"
91 Header set Access-Control-Allow-Methods "GET, POST, OPTIONS"
92 Header set Access-Control-Allow-Headers "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range,Content-Length"
93 Header set Access-Control-Expose-Headers "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range,Content-Length"
94
95 RewriteCond %{REQUEST_METHOD} OPTIONS
96 RewriteRule ^(.*)$ $1 [R=204,L]
97 </LocationMatch>
98
99 <LocationMatch /block/>
100 Header set Cache-Control "max-age=0"
101 </locationMatch>
102
103 RewriteRule ^/(register|login|settings|user|pad|drive|poll|slide|code|whiteboard|file|media|profile|contacts|todo|filepicker|debug|kanban|sheet|support|admin|notifications|teams|calendar|presentation|doc)$ $1/ [R=302,L]
104
105 RewriteCond %{DOCUMENT_ROOT}/www/%{REQUEST_URI} -f
106 RewriteRule (.*) /www/$1 [L]
107
108 RewriteCond %{DOCUMENT_ROOT}/www/%{REQUEST_URI}/index.html -f
109 RewriteRule (.*) /www/$1/index.html [L]
110
111 RewriteCond %{DOCUMENT_ROOT}/customize.dist/%{REQUEST_URI} -f
112 RewriteRule (.*) /customize.dist/$1 [L]
113
114 <Directory ${package}/lib/node_modules/cryptpad/www>
115 AllowOverride None
116 Require all granted
117 DirectoryIndex index.html
118 </Directory>
119 <Directory ${package}/lib/node_modules/cryptpad/customize.dist>
120 AllowOverride None
121 Require all granted
122 DirectoryIndex index.html
123 </Directory>
124 '';
125in
126{
127 options.myServices.tools.cryptpad.farm = {
128 hosts = lib.mkOption {
129 default = {};
130 description = "Hosts to install";
131 type = lib.types.attrsOf (lib.types.submodule {
132 options = {
133 port = lib.mkOption {
134 type = lib.types.port;
135 };
136 package = lib.mkOption {
137 type = lib.types.package;
138 description = "Cryptpad package to use";
139 default = pkgs.cryptpad;
140 };
141 domain = lib.mkOption {
142 type = lib.types.str;
143 description = "Domain for main host";
144 };
145 config = lib.mkOption {
146 type = lib.types.path;
147 description = "Path to configuration";
148 };
149 };
150 });
151 };
152 vhosts = lib.mkOption {
153 description = "Instance vhosts configs";
154 readOnly = true;
155 type = lib.types.attrsOf lib.types.str;
156 default = lib.genAttrs (builtins.attrNames cfg.hosts) toVhost;
157 };
158 vhostRoots = lib.mkOption {
159 description = "Instance vhosts document roots";
160 readOnly = true;
161 type = lib.types.attrsOf lib.types.path;
162 default = lib.genAttrs (builtins.attrNames cfg.hosts) toVhostRoot;
163 };
164 };
165 config = {
166 users.users = lib.optionalAttrs (cfg.hosts != {}) {
167 cryptpad = {
168 uid = config.ids.uids.cryptpad;
169 group = "cryptpad";
170 description = "Cryptpad user";
171 };
172 };
173 users.groups = lib.optionalAttrs (cfg.hosts != {}) {
174 cryptpad = {
175 gid = config.ids.gids.cryptpad;
176 };
177 };
178 systemd.services = lib.listToAttrs (map (n: lib.nameValuePair "cryptpad-${n}" (toService n)) (builtins.attrNames cfg.hosts));
179 };
180}
diff --git a/nixops/secrets b/nixops/secrets
Subproject dc2397900948d968bd5e1a32447d486edc626c4 Subproject 3bdfad457df0e47a409f1cf183e515b7a983683