From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [45.144.208.40]) by lore.proxmox.com (Postfix) with ESMTPS id 5E23B1FF0C1 for ; Wed, 26 Aug 2026 09:44:40 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id DA881214DD; Wed, 26 Aug 2026 09:44:31 +0200 (CEST) ARC-Seal: i=1; a=rsa-sha256; s=arcselector10001; d=microsoft.com; cv=none; b=uV7pU6uslSfbazjOGHRCJHLHsE+k1tiP/fZ9alknsMmLfR+VMxSJHCfnodDhwdHbvWDAs+JZ5cr6zzqMRqBXoUG7O/UIqvT7ArqBgHBZgYryWeyoaS+gxdg76CCeO7yjAWgESmZ98XsBXORxds9pLbD+uQU9G8kpEQM1m2Z7l2F4ACbbehs02DBeKITvZ8Do0b4XLRGSEu6VB8EiHUIrN/XZ+OBRYV3yTFauV32WGe5Cv0oswmcYvXUaQaS0MAuq9txfNqXOttOr8flDUX/VEiConTWUrF9tAXwRDVDhltT7ryA4dYIaoF9A6XKpFuct5SpTOZR/VLv+MrPTZ4Ncdw== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=microsoft.com; s=arcselector10001; h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-AntiSpam-MessageData-ChunkCount:X-MS-Exchange-AntiSpam-MessageData-0:X-MS-Exchange-AntiSpam-MessageData-1; bh=C79qd4DNCoLc4eckJFHKm7IGZQm70oKLZco/HVdZpj0=; b=fgTxKVl9j680tHYoZnYgqXnw/z1Hb5gp9vFhrj3K5Koltljt7G6zJpO4sUlANM8pviKlaQrOcrfijLNUl+7nXmoBbCg1ypkp849gqdOWDZFTgwODaawhRnbpDzzbygMXFW2e2eEaFahrg8wp3z0ZnhG/IxdjERJBsJ+KZBM0839ZhfuEQTJ8DNh5qLNqdagi05J5i8IPIrZJzS4FCBnJgepqX504p8F070/tEhvQpNIZscFuw/Sycao5mz4FVm5f5Sy6TxEn8zY5VI6VF99fWWkxZuYlsYf9egq4b5G2UWTDPJ5CaD+KUxDRuQr3F9Aff83VZcgNQG3uluauXOCn7w== ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=pass smtp.mailfrom=groupe-cyllene.com; dmarc=pass action=none header.from=groupe-cyllene.com; dkim=pass header.d=groupe-cyllene.com; arc=none DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=groupecyllene.onmicrosoft.com; s=selector2-groupecyllene-onmicrosoft-com; h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-SenderADCheck; bh=C79qd4DNCoLc4eckJFHKm7IGZQm70oKLZco/HVdZpj0=; b=kXzBGPu9tACrj306VFIy3RUmZt/bJ3m6XsGz20Ts2IDMcJ6XrCYEFJe7GzlQClKG7uJquf0cEsZigR5uRbj+M1fODbEUD7fYQoOO/kWVjEDAWm4U1fZgHVW3eKNuuABPLIvuw+XOvYi55ZqL0nJ2l4IHT6GOxrPKGP4Jctw+od8= Authentication-Results: dkim=none (message not signed) header.d=none;dmarc=none action=none header.from=groupe-cyllene.com; From: Alexandre Derumier To: pve-devel@lists.proxmox.com Subject: [RFC v2 pve-http-server 01/13] anyevent : proxy a path prefix to a local http proxy Date: Wed, 26 Aug 2026 09:43:33 +0200 Message-ID: <20260826074347.1256659-2-alexandre.derumier@groupe-cyllene.com> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260826074347.1256659-1-alexandre.derumier@groupe-cyllene.com> References: <20260826074347.1256659-1-alexandre.derumier@groupe-cyllene.com> Content-Transfer-Encoding: 8bit Content-Type: text/plain X-ClientProxiedBy: PA7P264CA0229.FRAP264.PROD.OUTLOOK.COM (2603:10a6:102:372::8) To PR1P264MB3696.FRAP264.PROD.OUTLOOK.COM (2603:10a6:102:143::11) MIME-Version: 1.0 X-MS-PublicTrafficType: Email X-MS-TrafficTypeDiagnostic: PR1P264MB3696:EE_|PR0P264MB2257:EE_ X-MS-Office365-Filtering-Correlation-Id: 92fa1297-b360-46e2-3ea2-08df0345d3c9 X-MS-Exchange-SenderADCheck: 1 X-MS-Exchange-AntiSpam-Relay: 0 X-Microsoft-Antispam: BCL:0;ARA:13230040|376014|23010399003|52116014|1800799024|366016|38350700014|6133799003|3023799007|56012099006|10067099003|5023799004|22082099003|18002099003; X-Microsoft-Antispam-Message-Info: QbIeqyNHmxOiwVW1r8Cnk8CybetL/VOYm7znZ8JYMFn0/w+N5gvNQLs68OhAnkVj3olGV0zIMIMwPbadhpKu+IIkTtlyf+WhpWO6+AtX+ySezIFYRoMOVeYo/rSejm9v6Vo/zTcxlzKF1Cc6gznr/igYP7jzxrrDbYlnpbdf6Mdlu1VvaTuREp/xTrfnhc2MqDHYQYZkcou0O8NJU3jlOD8JG6VGbhF2WoRPKFEHgVvSXzACkjb4/cWWaAUzqjpN6/sLRF1dFGiEA7pWUfkOG+CJ8n4M+9bevGh9Mbum5LvQ5NRqVOgHT91uTWKXmWIa2Vter+zI/VBK9Dqj1+Irx83LogDu1ulWdNfuZYX2hpygX7iQsVaAXF4TWf37QokaxntSfmv/qUOR0+o/NccWeDgDYjp2upNihc74hYM2NsOWfdiZCgx3IBJi/Ls0g7hXKCR6k4Vf95obKD5g22lqoqkq+Q45SG879xRM8mXj/xN0q7fUA4LSXDOWr3BhbqKuZCvzexF9gTc+aOvu2uDd934JOnVAJ8Z4j8U0r4hNbwnsf4nYUXJhm3UPnZEHdFypoFQzmA7H9//mKsH/wHpNAG7NN517Fr6xxM2Ps6XunjhDtMH13912g+bA/KEAFZSBhZBKgUaWNXNOrmU2hWcgL6qcaGDK0xM2+3+Gy6RO2EoUaiUYk5WeHOsPdIdBnD+R4+wOuwSFcLna2u/2Ct5nW9oQwsClm0gwDnCwUIRKoJ0= X-Forefront-Antispam-Report: CIP:255.255.255.255;CTRY:;LANG:en;SCL:1;SRV:;IPV:NLI;SFV:NSPM;H:PR1P264MB3696.FRAP264.PROD.OUTLOOK.COM;PTR:;CAT:NONE;SFS:(13230040)(376014)(23010399003)(52116014)(1800799024)(366016)(38350700014)(6133799003)(3023799007)(56012099006)(10067099003)(5023799004)(22082099003)(18002099003);DIR:OUT;SFP:1102; X-MS-Exchange-AntiSpam-MessageData-ChunkCount: 1 X-MS-Exchange-AntiSpam-MessageData-0: =?us-ascii?Q?8irN+QNORfcqEirf+YIMVVqCoRH4vru9fORyL0F3hnBOD7fX/graakYqxrcj?= =?us-ascii?Q?RBakIxm0XzQejPMTOU5Tqef2OFbXhHCnPY30rL11FU1QPHwzNFStNpz36zqC?= =?us-ascii?Q?QpuFQByGBlM6aunwQYmVjCmW7FfmRjB2dmG2Zzlj1P7mL3uhvAaOFUZyEyuE?= =?us-ascii?Q?n0XyZkqsX9GfgkwJkHFZnIVOMJN/LzUw3FHNXVBNBC+etX182pJvEeOZs79M?= =?us-ascii?Q?V6a7i7tUVWGmqlc1m/NlusAki46IenV9nm0vKj329tEBJQTr3JMcV0gYcxjG?= =?us-ascii?Q?F9wzJQRoikFBjEyhqjUyLcH+QF+LUc/WeRxwUseJhtzaAWgDNpzF9OeHLGwG?= =?us-ascii?Q?A6i6lOZCLdvcdqCuWjQFy8kkmqeag+jdOq8SlnRMlwCy0Oz8IyQtTsMtWeSZ?= =?us-ascii?Q?99ScMiApJXKsn2lxt2cm74p+UVIaBJ0+r6yvvk4d8vUXxdzpLRemHXEXjMVS?= =?us-ascii?Q?abRHtQEHBc7f/0Lj2a22U6hyPw1YUi9rMtaYFt79TFmTJ1mHidyqWUMd4i3h?= =?us-ascii?Q?V+Qa9n3QA55L4W7Sg45yuFuCoI9QzhLEVujovX7m7UeH9uy7gHxbrjMSQOjm?= =?us-ascii?Q?kpwTkpIsFHfksS9PseN5jS8nqpfdfuNKmuj2PMXf6VEqnbvmujr8aFBltrgu?= =?us-ascii?Q?Smr5Hc04lRSUZRz3cclWoKnOjXqAvXetE9tmj4TcSNE8c7m6hqBdTFpgDzS+?= =?us-ascii?Q?AGvxjLiOYZY0sKWaRFERU4QylDtGbPJ29SPR9XcchiMbD6u3mrry1UT3cxnf?= =?us-ascii?Q?h5K+fsm1D8IIUD6qbFvNE09EVBkoT7KqAkk4wn6GcH1ScF9UiBxDHUB/cdjh?= =?us-ascii?Q?dO9jJrkh8FyLpniMBx53RnUmvBbqOE+e41n1sNebnZG3wzzNvRzWtbzRDQqp?= =?us-ascii?Q?m6sfIDKzO5Wsn6RSNgce1fHiS7yL4c+fNklGbjCOdrTlnUoZ3vAhyzfYXIMu?= =?us-ascii?Q?vOpXly0pBzBn63uDLpGhJlFFmd9G4PgnkLS8JPCpeA+CL348dSEjl/dL3R4w?= =?us-ascii?Q?W8ETRTrZfFmzaIIIDFQlbZXGaPZ+PoggQt2EafgP0V/Lql/txBec5l0JHyNM?= =?us-ascii?Q?283qf6rxP3qHKmbOW+x6xgBtTneX2KpZpDGZCQyc9kcmB+4lMz8/o7LdglVL?= =?us-ascii?Q?JGG060FF3XmKWUcvY2emJKd7lF7Mulu0upJPfPYDeoQsIZ+VApIF0Fnha+q8?= =?us-ascii?Q?ZNJ5n+vQm3yZaoRZJOS5GGnVktl7xNwcs8idLStw+doiQ7Kr8JkYMdcD34Hj?= =?us-ascii?Q?2mjQIv3eAdwJRZngsbXkobWGEB2c3JBPwYZmO7FG2qmMkstmC1iXVgXrhy6g?= =?us-ascii?Q?rn2b2Q7S5UUrBEYu3MZKW2FkEU3Q0nMOmxGYETrNAnri83o4/0nK3rsZ69wI?= =?us-ascii?Q?b6NdpXytm062KTw6O86roig8UxrjyBKiB6bFo7l832fxjtusXlnpXs9VYygB?= =?us-ascii?Q?Ex+hWumtZrzK1pwUYCyQcUe12C7vQKKrVtuT+Y0M+EGeoCAzW/wdQM4LhVlf?= =?us-ascii?Q?NG0W+MNtws3/qyr5GEJKOZCds7uXSBaXpzGjf6TZcoyY23ynA8QROUxwv1mm?= =?us-ascii?Q?8HfOPwFTcpiCbPA797KpXOZwu0Fh3OZB0Mm/ZHbszP6lJNWJOHgBulrSbnrT?= =?us-ascii?Q?6torlyczYxxuY07XaQS0vEmjLvsz5shht9T8MY0VNUZIwqf5dImu6yHh7xkp?= =?us-ascii?Q?uCncO8a7MH3xo6+l2+9N3L2e6PQLKvMYB0UE4ptW2JGyGC6/v2w1RroYe3jL?= =?us-ascii?Q?WLssEKYwqj8n8xHbvyDvB0T33qKxTlBWPjcLJp5Z2qTzJZ9qlgEo?= X-OriginatorOrg: groupe-cyllene.com X-MS-Exchange-CrossTenant-Network-Message-Id: 92fa1297-b360-46e2-3ea2-08df0345d3c9 X-MS-Exchange-CrossTenant-AuthSource: PR1P264MB3696.FRAP264.PROD.OUTLOOK.COM X-MS-Exchange-CrossTenant-AuthAs: Internal X-MS-Exchange-CrossTenant-OriginalArrivalTime: 26 Aug 2026 07:44:15.2062 (UTC) X-MS-Exchange-CrossTenant-FromEntityHeader: Hosted X-MS-Exchange-CrossTenant-Id: ee11ccf7-112c-4284-848b-f229745e715b X-MS-Exchange-CrossTenant-MailboxType: HOSTED X-MS-Exchange-CrossTenant-UserPrincipalName: OmaxdC9HlEr6mHESJqpruOaQjPfKHRTpwR1mrmTLq2vnIi/OEecTsuK7aay+7FekDLWnFHKnAkFa68NkEWiwjXACfEd17WaaMKEyxQfwlIWOTcY3q/KKIXj+DvBdfDqZ X-MS-Exchange-Transport-CrossTenantHeadersStamped: PR0P264MB2257 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.188 Adjusted score from AWL reputation of From: address DKIM_SIGNED 0.1 Message has a DKIM or DK signature, not necessarily valid DKIM_VALID -0.1 Message has at least one valid DKIM or DK signature DMARC_PASS -0.1 DMARC pass policy RCVD_IN_DNSWL_NONE -0.0001 Sender listed at https://www.dnswl.org/, no trust SPF_HELO_PASS -0.001 SPF: HELO matches SPF record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: 5DY3ER326FSAZYVQ45RFCSFQZUZTESXA X-Message-ID-Hash: 5DY3ER326FSAZYVQ45RFCSFQZUZTESXA X-MailFrom: Alexandre.DERUMIER@groupe-cyllene.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: A handler says which prefixes go to a local backend and who may reach them, so a service can use this server's TLS and authentication without its own port. Requests go verbatim, not re-encoded like proxy_request; an upgrade becomes a pipe after 101, and the relay stops reading while the far side is behind. Signed-off-by: Alexandre Derumier --- src/PVE/APIServer/AnyEvent.pm | 263 ++++++++++++++++++++++++++++++++++ 1 file changed, 263 insertions(+) diff --git a/src/PVE/APIServer/AnyEvent.pm b/src/PVE/APIServer/AnyEvent.pm index 915d678..dc95c12 100644 --- a/src/PVE/APIServer/AnyEvent.pm +++ b/src/PVE/APIServer/AnyEvent.pm @@ -731,6 +731,250 @@ sub websocket_proxy { } } +# Queued for one side before the other stops being read. Smaller than +# response_stream's 4MB: consoles are not downloads, and a backlog is only +# latency the far end could have coalesced away. +my $relay_buf_size = 1024 * 1024; + +# What a handle still owes its socket; TLS keeps a second buffer. +sub relay_queued { + my ($hdl) = @_; + return length($hdl->{wbuf}) + length($hdl->{_tls_wbuf} // ''); +} + +# One direction of an upgraded connection: copy to the other side, and stop +# reading while that side is behind, so back pressure reaches the far end +# instead of queueing here. Same shape as response_stream, and named rather +# than a closure over itself, which would be a cycle. The handles come from +# callbacks because either may be gone by the time this runs. +sub relay_reader { + my ($from, $to) = @_; + + return sub { + my ($hdl) = @_; + + my $writer = $to->(); + return if !$writer; + + my $data = $hdl->{rbuf}; + $hdl->{rbuf} = ''; + $writer->push_write($data) if length($data); + + return if relay_queued($writer) < $relay_buf_size; + + my $prev_on_drain = $writer->{on_drain}; + $writer->on_drain(sub { + my ($wrhdl) = @_; + # Restored first: setting on_drain runs it on an empty buffer. + $wrhdl->on_drain($prev_on_drain); + if (my $reader = $from->()) { + $reader->on_read(relay_reader($from, $to)); + } + }); + + $hdl->on_read(); + }; +} + +# Hand an upgrade to the backend: the request goes out as it arrived and the +# answer comes back untouched, so the two ends compute the accept key. After +# 101 this is a pipe, which knows nothing of websockets. +sub local_http_proxy_upgrade { + my ($self, $reqstate, $method, $target) = @_; + + my $r = $reqstate->{request}; + + my ($remhost, $remport); + if ($target->{port}) { + $remhost = 'localhost'; + $remport = $target->{port}; + } else { + $remhost = 'unix/'; + $remport = $target->{socket}; + } + my $path = $target->{path} // '/'; + + # Only Host is rewritten: this is the hop being upgraded, so Connection + # and Upgrade stay. + my $headers = ''; + $r->headers->scan(sub { + my ($key, $value) = @_; + return if lc($key) eq 'host'; + $headers .= "$key: $value\015\012"; + }); + my $request = "$method $path HTTP/1.1\015\012Host: localhost\015\012$headers\015\012"; + + tcp_connect $remhost, $remport, sub { + my ($fh) = @_ + or do { + $self->error($reqstate, HTTP_BAD_GATEWAY, "connect to backend failed: $!"); + return; + }; + + $reqstate->{proxyhdl} = AnyEvent::Handle->new( + fh => $fh, + rbuf_max => 64 * 1024, + wbuf_max => 4 * $relay_buf_size, + timeout => 30, + on_eof => sub { + eval { + $self->log_aborted_request($reqstate); + $self->client_do_disconnect($reqstate); + }; + warn $@ if $@; + }, + on_error => sub { + my ($hdl, $fatal, $message) = @_; + eval { + $self->log_aborted_request($reqstate, $message); + $self->client_do_disconnect($reqstate); + }; + warn $@ if $@; + }, + ); + + $reqstate->{proxyhdl}->push_write($request); + + $reqstate->{proxyhdl}->push_read( + line => "\015\012\015\012", + sub { + my ($hdl, $response) = @_; + + # Only 101 means the backend stopped speaking HTTP. + if ($response !~ m|^HTTP/1\.1 101|) { + my ($status) = $response =~ m|^(\S+ \d+[^\015]*)|; + $self->log_aborted_request($reqstate, + "backend refused upgrade: " . ($status // 'unparseable response')); + $self->client_do_disconnect($reqstate); + return; + } + + # Verbatim: it carries the accept key for the client's key. + $reqstate->{hdl}->push_write($response . "\015\012\015\012"); + + $reqstate->{proxyhdl}->timeout(0); + $reqstate->{hdl}->timeout(0); + + my $client = sub { $reqstate->{hdl} }; + my $backend = sub { $reqstate->{proxyhdl} }; + + $reqstate->{proxyhdl}->on_read(relay_reader($backend, $client)); + $reqstate->{hdl}->on_read(relay_reader($client, $backend)); + + $reqstate->{log}->{code} = 101; + $self->log_request($reqstate); + }, + ); + }; + + return; +} + +# Forward a request verbatim to a service on loopback, unlike proxy_request, +# which re-encodes parsed parameters for another PVE node. The backend is a +# foreign HTTP server, kept behind this server's TLS and authentication. +sub local_http_proxy_request { + my ($self, $reqstate, $method, $target) = @_; + + my $r = $reqstate->{request}; + + my $port = $target->{port}; + my $socket = $target->{socket}; + die "local_http_proxy_request: missing port or socket\n" if !$port && !$socket; + my $path = $target->{path} // '/'; + my $scheme = $target->{tls} ? 'https' : 'http'; + + if ($r->header('upgrade')) { + $self->local_http_proxy_upgrade($reqstate, $method, $target); + return; + } + + # Hop-by-hop headers describe the connection they arrived on, and + # Accept-Encoding goes too, so this server can compress the body itself. + my $skip = { + map { $_ => 1 } qw( + connection keep-alive host content-length transfer-encoding + upgrade te trailer proxy-authorization accept-encoding + ) + }; + + # A unix socket has no authority to name, and nothing behind here routes on + # Host anyway. + my $headers = { Host => $port ? "127.0.0.1:$port" : 'localhost' }; + $r->headers->scan(sub { + my ($key, $value) = @_; + $headers->{$key} = $value if !$skip->{ lc($key) }; + }); + + my $content = $r->content; + $headers->{'Content-Length'} = length($content) if length($content); + + my $tls_ctx; + if ($target->{tls}) { + # Loopback, with a certificate no browser sees and no CA signed: there + # is nothing verification could check. + $tls_ctx = AnyEvent::TLS->new(method => 'any', sslv2 => 0, sslv3 => 0, verify => 0); + } + + # AnyEvent::HTTP needs a URL to parse, so a unix backend gets a nominal + # authority and a tcp_connect that ignores it. + my $url = $port ? "$scheme://127.0.0.1:$port$path" : "$scheme://localhost$path"; + my $tcp_connect; + if ($socket) { + $tcp_connect = sub { + my (undef, undef, $connect_cb, $prepare_cb) = @_; + return AnyEvent::Socket::tcp_connect('unix/', $socket, $connect_cb, $prepare_cb); + }; + } + + my $w; + $w = http_request( + $method => $url, + headers => $headers, + $tcp_connect ? (tcp_connect => $tcp_connect) : (), + timeout => 30, + proxy => undef, # avoid use of $ENV{HTTP_PROXY} + persistent => 0, + keepalive => 0, + body => length($content) ? $content : undef, + $tls_ctx ? (tls_ctx => $tls_ctx) : (), + sub { + my ($body, $hdr) = @_; + + undef $w; + + if (!$reqstate->{hdl}) { + warn "local http proxy detected vanished client connection\n"; + return; + } + + eval { + my $code = delete $hdr->{Status}; + my $msg = delete $hdr->{Reason}; + delete $hdr->{URL}; + delete $hdr->{HTTPVersion}; + + # AnyEvent::HTTP reports its own failures in the 59x range. + if ($code >= 590) { + $self->error($reqstate, HTTP_BAD_GATEWAY, "$msg"); + return; + } + + # Set by this server for the connection it answers on. + delete $hdr->{$_} for qw(connection transfer-encoding content-length); + + my $header = HTTP::Headers->new(%$hdr); + my $resp = HTTP::Response->new($code, $msg, $header, $body); + # Note: disable compression, the backend decides its own encoding + $self->response($reqstate, $resp, undef, 1); + }; + warn $@ if $@; + }, + ); + + return; +} + sub proxy_request { my ($self, $reqstate, $clientip, $host, $node, $method, $uri, $auth, $params) = @_; @@ -1222,6 +1466,25 @@ sub handle_request { # we re-enable timeout in response() $reqstate->{hdl}->timeout(0); + # The handler says where to send it, or nothing for the usual dispatch. + if (my $handler = $self->{local_http_proxy_handler}) { + my $target = eval { $handler->($self, $reqstate, $auth, $method, $path) }; + if (my $err = $@) { + # The handler's refusals are answers: a denial must stay 403. + my $code = HTTP_INTERNAL_SERVER_ERROR; + if (ref($err) && eval { $err->{code} }) { + my $carried = $err->{code}; + $code = $carried if $carried =~ m/^\d+$/ && $carried >= 400 && $carried <= 599; + } + $self->error($reqstate, $code, "$err"); + return; + } + if ($target) { + $self->local_http_proxy_request($reqstate, $method, $target); + return; + } + } + if ($path =~ m/^\Q$base_uri\E/) { $self->handle_api2_request($reqstate, $auth, $method, $path); return; -- 2.55.0