diff --git a/eng/pipelines/libraries/enterprise/linux.yml b/eng/pipelines/libraries/enterprise/linux.yml
index 40552c1a0e18f8..3fc1b387e4bc17 100644
--- a/eng/pipelines/libraries/enterprise/linux.yml
+++ b/eng/pipelines/libraries/enterprise/linux.yml
@@ -19,6 +19,7 @@ pr:
- src/native/libs/System.Net.Security.Native/*
- src/libraries/System.Net.Http/*
- src/libraries/System.Net.Security/*
+ - src/libraries/System.Net.Requests/*
variables:
- template: ../variables.yml
@@ -63,12 +64,14 @@ extends:
docker exec linuxclient bash -c '/repo/build.sh -subset clr+libs -runtimeconfiguration release -ci /p:NativeOptimizationDataSupported=false'
docker exec linuxclient bash -c '/repo/dotnet.sh build $(containerLibrariesRoot)/System.Net.Http/tests/EnterpriseTests/System.Net.Http.Enterprise.Tests.csproj'
docker exec linuxclient bash -c '/repo/dotnet.sh build $(containerLibrariesRoot)/System.Net.Security/tests/EnterpriseTests/System.Net.Security.Enterprise.Tests.csproj'
+ docker exec linuxclient bash -c '/repo/dotnet.sh build $(containerLibrariesRoot)/System.Net.Requests/tests/EnterpriseTests/System.Net.Requests.Enterprise.Tests.csproj'
displayName: Build product sources
- bash: |
docker exec linuxclient bash -c 'if [ -f /erc/resolv.conf.ORI ]; then cp -f /erc/resolv.conf.ORI /etc/resolv.conf; fi'
docker exec linuxclient $(containerRunTestsCommand) $(containerLibrariesRoot)/System.Net.Http/tests/EnterpriseTests/System.Net.Http.Enterprise.Tests.csproj
docker exec linuxclient $(containerRunTestsCommand) $(containerLibrariesRoot)/System.Net.Security/tests/EnterpriseTests/System.Net.Security.Enterprise.Tests.csproj
+ docker exec linuxclient $(containerRunTestsCommand) $(containerLibrariesRoot)/System.Net.Requests/tests/EnterpriseTests/System.Net.Requests.Enterprise.Tests.csproj
displayName: Build and run tests
- bash: |
diff --git a/src/libraries/Common/tests/System/Net/EnterpriseTests/setup/apacheweb/Dockerfile b/src/libraries/Common/tests/System/Net/EnterpriseTests/setup/apacheweb/Dockerfile
index 8be1402f8d6cc5..45eb370fab7490 100644
--- a/src/libraries/Common/tests/System/Net/EnterpriseTests/setup/apacheweb/Dockerfile
+++ b/src/libraries/Common/tests/System/Net/EnterpriseTests/setup/apacheweb/Dockerfile
@@ -1,24 +1,48 @@
-FROM mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-18.04
+FROM mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-24.04
ARG DEBIAN_FRONTEND=noninteractive
-# Install Kerberos client, apache Negotiate auth plugin, and diagnostics
+# Install Kerberos client, ProFTPD with SSL, and diagnostics
RUN apt-get update && \
- apt-get install -y --no-install-recommends apache2 libapache2-mod-auth-kerb procps krb5-user iputils-ping dnsutils nano \
- libapache2-mod-auth-ntlm-winbind samba samba-dsdb-modules samba-vfs-modules
+ apt-get install -y --no-install-recommends apache2 procps krb5-user iputils-ping dnsutils nano \
+ samba samba-dsdb-modules samba-vfs-modules \
+ proftpd-basic proftpd-mod-crypto openssl \
+ apache2-dev libapache2-mod-auth-gssapi samba-ad-provision winbind
WORKDIR /setup
COPY ./common/krb5.conf /etc/
COPY ./apacheweb/apache2.conf /setup/apache2.conf
COPY ./apacheweb/*.sh ./
+COPY ./apacheweb/mod_auth_ntlm_winbind/mod_auth_ntlm_winbind.c /tmp
RUN chmod +x *.sh ; \
mkdir -p /setup/htdocs/auth/ntlm /setup/altdocs/auth/ntlm /setup/htdocs/auth/kerberos /setup/altdocs/auth/kerberos /setup/htdocs/auth/digest ;\
touch /setup/htdocs/index.html /setup/htdocs/auth/kerberos/index.html /setup/htdocs/auth/ntlm/index.html /setup/htdocs/auth/digest/index.html ;\
touch /setup/altdocs/auth/kerberos/index.html /setup/altdocs/auth/ntlm/index.html ;\
cp /etc/apache2/apache2.conf /etc/apache2/apache2.conf.ORIG ;\
- mv -f apache2.conf /etc/apache2/apache2.conf
+ mv -f apache2.conf /etc/apache2/apache2.conf ;\
+ cd /tmp && apxs -DAPACHE2 -c -i mod_auth_ntlm_winbind.c
+
+# Setup FTP user and directories
+RUN useradd -m -s /bin/bash ftpuser && \
+ echo "ftpuser:ftppass" | chpasswd && \
+ mkdir -p /home/ftpuser/ftp && \
+ chown ftpuser:ftpuser /home/ftpuser/ftp && \
+ mkdir -p /var/log/proftpd && \
+ chmod 755 /var/log/proftpd
+
+# Generate self-signed certificate for FTP TLS
+RUN mkdir -p /etc/proftpd/ssl && \
+ openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
+ -keyout /etc/proftpd/ssl/proftpd.key \
+ -out /etc/proftpd/ssl/proftpd.crt \
+ -subj "/C=US/ST=State/L=City/O=Test/CN=apacheweb.linux.contoso.com"
+
+# Copy ProFTPD configuration
+COPY ./apacheweb/proftpd.conf /etc/proftpd/proftpd.conf
EXPOSE 80/tcp
EXPOSE 8080/tcp
+EXPOSE 21/tcp
+EXPOSE 50000-50100/tcp
ENTRYPOINT ["/bin/bash", "/setup/run.sh"]
diff --git a/src/libraries/Common/tests/System/Net/EnterpriseTests/setup/apacheweb/apache2.conf b/src/libraries/Common/tests/System/Net/EnterpriseTests/setup/apacheweb/apache2.conf
index a26a52ed8a0c9f..acec5460c728a8 100644
--- a/src/libraries/Common/tests/System/Net/EnterpriseTests/setup/apacheweb/apache2.conf
+++ b/src/libraries/Common/tests/System/Net/EnterpriseTests/setup/apacheweb/apache2.conf
@@ -2,13 +2,13 @@
# This is the main Apache HTTP server configuration file. It contains the
# configuration directives that give the server its instructions.
# See for detailed information.
-# In particular, see
+# In particular, see
#
# for a discussion of each configuration directive.
#
# Do NOT simply read the instructions in here without understanding
# what they do. They're here only as hints or reminders. If you are unsure
-# consult the online docs. You have been warned.
+# consult the online docs. You have been warned.
#
# Configuration and logfile names: If the filenames you specify for many
# of the server's control files begin with "/" (or "drive:/" for Win32), the
@@ -206,7 +206,7 @@ LoadModule alias_module modules/mod_alias.so
LoadModule auth_ntlm_winbind_module modules/mod_auth_ntlm_winbind.so
-LoadModule auth_kerb_module modules/mod_auth_kerb.so
+LoadModule auth_gssapi_module modules/mod_auth_gssapi.so
#
@@ -285,11 +285,10 @@ DocumentRoot "/setup/htdocs"
Options Indexes FollowSymLinks
AllowOverride None
- AuthType Kerberos
+ AuthType GSSAPI
AuthName "Kerberos Login"
- KrbAuthRealm LINUX.CONTOSO.COM
- Krb5Keytab /etc/krb5.keytab
- KrbMethodK5Passwd off
+ GssapiCredStore keytab:/etc/krb5.keytab
+ GssapiBasicAuth Off
Require valid-user
@@ -304,7 +303,7 @@ DocumentRoot "/setup/htdocs"
AuthName "NTLM Authentication"
AuthType NTLN
NTLMauth on
- NTLMAuthHelper "/usr/bin/ntlm_auth --helper-protocol=squid-2.5-ntlmssp --diagnostic"
+ NTLMAuthHelper "/usr/bin/ntlm_auth --helper-protocol=squid-2.5-ntlmssp "
Require valid-user
@@ -312,12 +311,11 @@ DocumentRoot "/setup/htdocs"
Options Indexes FollowSymLinks
AllowOverride None
- AuthType Kerberos
+ AuthType GSSAPI
AuthName "Kerberos Login"
- KrbAuthRealm LINUX.CONTOSO.COM
- Krb5Keytab /etc/krb5.keytab
- KrbMethodK5Passwd off
- KrbServiceName HTTP/altweb.linux.contoso.com:8080
+ GssapiCredStore keytab:/etc/krb5.keytab
+ GssapiBasicAuth Off
+ GssapiAcceptorName HTTP@altweb.linux.contoso.com:8080
Require valid-user
@@ -428,12 +426,11 @@ LogLevel warn
AllowOverride None
Options None
- AuthType Kerberos
-AuthName "Kerberos Login"
-KrbAuthRealm LINUX.CONTOSO.COM
-Krb5Keytab /etc/krb5.keytab
-KrbMethodK5Passwd off
-Require valid-user
+ AuthType GSSAPI
+ AuthName "Kerberos Login"
+ GssapiCredStore keytab:/etc/krb5.keytab
+ GssapiBasicAuth Off
+ Require valid-user
diff --git a/src/libraries/Common/tests/System/Net/EnterpriseTests/setup/apacheweb/mod_auth_ntlm_winbind/README b/src/libraries/Common/tests/System/Net/EnterpriseTests/setup/apacheweb/mod_auth_ntlm_winbind/README
new file mode 100644
index 00000000000000..a5cc171059f19d
--- /dev/null
+++ b/src/libraries/Common/tests/System/Net/EnterpriseTests/setup/apacheweb/mod_auth_ntlm_winbind/README
@@ -0,0 +1,101 @@
+OVERVIEW
+
+The mod_auth_ntlm_winbind module provides authentication and
+authorisation over the web against a Microsoft Windows NT/2000/XP or
+Samba Domain Controller using Samba's winbind daemon running on the
+same machine Apache 1.x or 2.x is running on.
+
+Used only by IE and newer versions of the Mozilla browser family, the
+NTLM over HTTP protocol is completed undocumented by Microsoft but has
+been reverse engineered and described at the following URL:
+
+http://davenport.sf.net/ntlm.html
+
+
+INSTALLATION
+
+The configure.in script and Makefile are essentially wrappers around
+apxs, which should be able to do all the work by itself. Having said
+that, the build/install process should simply be a matter of:
+
+$ autoconf
+$ ./configure
+$ make
+$ sudo make install
+
+The configure script will attempt to locate apxs and httpd. It will
+prefer apxs2 to apxs, and will use the httpd it finds to determine
+whether it is building for Apache 1 or Apache 2. You can override the
+detected settings using --with-apxs=/path/to/apxs and
+--with-httpd=/path/to/httpd
+
+In the event that the configure/Make combination doesn't work, you
+should be able to do:
+
+[Apache 1.x]
+$ apxs -c -i mod_auth_ntlm_winbind.c
+
+[Apache 2.x]
+$ apxs -DAPACHE2 -c -i mod_auth_ntlm_winbind.c
+(substitute apxs2 as appropriate)
+
+
+CONFIGURATION
+
+mod_auth_ntlm_winbind uses the same ntlm_auth helper as the Squid
+proxy, so the same setup applies as for Squid: the winbindd_privileged
+directory must be accessible by the webserver userid. The
+configuration directives added by this module are as follows:
+
+NTLMAuth
+ set to 'on' to activate NTLM authentication
+NegotiateAuth
+ set to 'on' to activate Negotiate authentication
+NTLMBasicAuthoritative
+ set to 'off' to allow access control to be passed along to lower
+ modules if the UserID is not known to this module
+NTLMBasicAuth
+ set to 'on' to activate Basic authentication (for non-NTLM browsers)
+NTLMBasicRealm
+ Realm to use for Basic authentication
+NTLMAuthHelper
+ Location and arguments to the Samba ntlm_auth utility for NTLM auth
+NegotiateAuthHelper
+ Location and arguments to the Samba ntlm_auth utility for Negotiate auth
+PlaintextAuthHelper
+ Location and arguments to the Samba ntlm_auth utility for Plaintext auth
+
+
+The following httpd.conf configuration describes an example
+configuration for this module:
+
+NTLM authentication:
+
+
+ AuthName "NTLM Authentication thingy"
+ NTLMAuth on
+ NTLMAuthHelper "/usr/bin/ntlm_auth --helper-protocol=squid-2.5-ntlmssp"
+ NTLMBasicAuthoritative on
+ AuthType NTLM
+ require valid-user
+
+
+or, to enable 'NTLM+Negotiate' authentication too:
+
+
+ AuthName "NTLM Authentication thingy"
+ NTLMAuth on
+ NegotiateAuth on
+ NTLMAuthHelper "/usr/bin/ntlm_auth --helper-protocol=squid-2.5-ntlmssp"
+ NegotiateAuthHelper "/usr/bin/ntlm_auth --helper-protocol=gss-spnego"
+ NTLMBasicAuthoritative on
+ AuthType NTLM
+ AuthType Negotiate
+ require valid-user
+
+
+
+To debug what is going on, add the following line to your httpd.conf
+to enable debug messages to be written to the apache error log file:
+
+LogLevel debug
diff --git a/src/libraries/Common/tests/System/Net/EnterpriseTests/setup/apacheweb/mod_auth_ntlm_winbind/mod_auth_ntlm_winbind.c b/src/libraries/Common/tests/System/Net/EnterpriseTests/setup/apacheweb/mod_auth_ntlm_winbind/mod_auth_ntlm_winbind.c
new file mode 100644
index 00000000000000..5749229137f461
--- /dev/null
+++ b/src/libraries/Common/tests/System/Net/EnterpriseTests/setup/apacheweb/mod_auth_ntlm_winbind/mod_auth_ntlm_winbind.c
@@ -0,0 +1,1084 @@
+/*
+ * mod_auth_ntlm_winbind.c -- Apache auth_ntlm_winbind module
+ *
+ * [Originally autogenerated via ``apxs -n ntlm_winbind -g'']
+ *
+ * Based on:
+ * mod_ntlm.c for Win32 by Tim Costello
+ * pam_smb by Dave Airlie
+ *
+ * Copyright Andreas Gal , 2000
+ * Copyright Sverre H. Huseby , 2000
+ * Copyright Tim Potter , 2001
+ * Copyright Andrew Bartlett , 2004
+ * Apache2 code by Waider , 2006
+ *
+ * THIS SOFTWARE IS PROVIDED ``AS IS`` AND ANY EXPRESSED OR IMPLIED
+ * WARRANTIES ARE DISCLAIMED.
+ *
+ * This code may be freely distributed, as long the above notices are
+ * reproduced.
+ */
+/* (CGI fork code)
+ * Copyright 1999-2004 The Apache Software Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+/*
+ * The sections OVERVIEW, INSTALLATION, and CONFIGURATION are available from
+ * the README file in this directory.
+ */
+
+#include "httpd.h"
+#include "http_config.h"
+#include "http_protocol.h"
+#include "http_log.h"
+#include "http_core.h"
+#include "ap_config.h"
+#include "util_script.h" /* for ap_call_exec */
+#include
+
+#ifdef APACHE2
+#include "http_request.h"
+#include "http_connection.h"
+#include "apr_strings.h"
+#include "apr_pools.h"
+#include "apr_tables.h"
+#include "apr_base64.h"
+
+#define RDEBUG( x... ) ap_log_rerror( APLOG_MARK, NTLM_DEBUG, APR_SUCCESS, r, x )
+#define RERROR( c, x... ) ap_log_rerror( APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, c, r, x )
+#define CLEANUP(x) NULL
+#else
+
+/* compat stuff */
+#define apr_pool_t ap_pool
+#define apr_pstrcat(x...) ap_pstrcat(x)
+#define apr_pstrdup(x...) ap_pstrdup(x)
+#define apr_psprintf(x...) ap_psprintf(x)
+#define apr_table_add(x...) ap_table_add(x)
+#define apr_table_get(x...) ap_table_get(x)
+#define apr_table_setn(x...) ap_table_setn(x)
+#define apr_pcalloc(x...) ap_pcalloc(x)
+#define apr_pool_destroy(x...) ap_destroy_pool(x)
+
+#define RDEBUG( x... ) ap_log_rerror( APLOG_MARK, LOG_DEBUG, r, x )
+#define RERROR( c, x... ) ap_log_rerror( APLOG_MARK, NTLM_DEBUG|APLOG_NOERRNO, r, x )
+#define CLEANUP(x) x
+#endif
+
+/* The name of the NTLM authentication scheme. This appears in the
+ 'WWW-Authenticate' header in the initial HTTP request. */
+
+#define NTLM_AUTH_NAME "NTLM"
+#define NEGOTIATE_AUTH_NAME "Negotiate"
+
+/* A structure to hold information about the configuration for the
+ mod_auth_ntlm_winbind apache module. */
+
+typedef struct _ntlm_config_struct {
+ unsigned int ntlm_on;
+ unsigned int negotiate_on;
+ unsigned int ntlm_basic_on;
+ char *ntlm_basic_realm;
+ unsigned int authoritative;
+ char *ntlm_auth_helper;
+ char *negotiate_ntlm_auth_helper;
+ char *ntlm_plaintext_helper;
+} ntlm_config_rec;
+
+/* A structure to hold per-connection information about authentications
+ that are in progress. */
+
+struct _ntlm_auth_helper {
+ int sent_challenge;
+ int helper_pid;
+#ifdef APACHE2
+ apr_proc_t *proc;
+#else
+ BUFF *out_to_helper, *in_from_helper;
+#endif
+ apr_pool_t *pool;
+};
+
+struct _connected_user_authenticated {
+ char *user;
+ char *auth_type;
+ apr_pool_t *pool;
+ int keepalives; /* used to detect redirected auths */
+};
+
+struct _ntlm_child_stuff {
+ request_rec *r;
+ char *argv0;
+};
+
+typedef struct _conn_context {
+ struct _connected_user_authenticated *connected_user_authenticated;
+} ntlm_connection_context_t;
+
+typedef struct _ntlm_context {
+ struct _ntlm_auth_helper *ntlm_auth_helper;
+ struct _ntlm_auth_helper *negotiate_ntlm_auth_helper;
+ struct _ntlm_auth_helper *ntlm_plaintext_helper;
+} ntlm_context_t;
+
+#ifdef APACHE2
+module AP_MODULE_DECLARE_DATA auth_ntlm_winbind_module;
+#else
+module MODULE_VAR_EXPORT auth_ntlm_winbind_module;
+#endif
+
+#define NTLM_DEBUG (APLOG_DEBUG | APLOG_NOERRNO)
+
+/* If we have already authenticated then allow all subsequence accesses.
+ This appears to be what IE and IIS do when talking to each other. I
+ don't think there are any security problems with this, unless someone
+ hijacks the connection, but then MS is susceptible to exactly the same
+ problem. */
+
+/* Extra apache configuration directives defined for this module */
+static const command_rec ntlm_winbind_cmds[] = {
+#ifdef APACHE2
+ /* NTLM authentication commands */
+ AP_INIT_FLAG( "NTLMAuth", ap_set_flag_slot,
+ (void *) APR_OFFSETOF( ntlm_config_rec, ntlm_on ),
+ OR_AUTHCFG,
+ "set to 'on' to activate NTLM authentication" ),
+
+ AP_INIT_FLAG( "NegotiateAuth", ap_set_flag_slot,
+ (void *) APR_OFFSETOF(ntlm_config_rec, negotiate_on),
+ OR_AUTHCFG,
+ "set to 'on' to activate Negotiate authentication" ),
+
+ AP_INIT_FLAG( "NTLMBasicAuthoritative", ap_set_flag_slot,
+ (void *) APR_OFFSETOF(ntlm_config_rec, authoritative),
+ OR_AUTHCFG,
+ "set to 'off' to allow access control to be passed along to lower "
+ "modules if the UserID is not known to this module" ),
+ /* ntlm_auth location */
+ AP_INIT_TAKE1( "NTLMAuthHelper", ap_set_string_slot,
+ (void *) APR_OFFSETOF(ntlm_config_rec, ntlm_auth_helper),
+ OR_AUTHCFG,
+ "location and arguments to the Samba ntlm_auth utility" ),
+
+ AP_INIT_TAKE1( "NegotiateAuthHelper", ap_set_string_slot,
+ (void *) APR_OFFSETOF(ntlm_config_rec, negotiate_ntlm_auth_helper),
+ OR_AUTHCFG,
+ "location and arguments to the Samba ntlm_auth utility" ),
+
+ AP_INIT_TAKE1( "PlaintextAuthHelper", ap_set_string_slot,
+ (void *) APR_OFFSETOF(ntlm_config_rec, ntlm_plaintext_helper ),
+ OR_AUTHCFG,
+ "location and arguments to the Samba ntlm_auth utility" ),
+
+ /* Basic Authentication transport for non-IE browsers */
+ AP_INIT_FLAG( "NTLMBasicAuth", ap_set_flag_slot,
+ (void *) APR_OFFSETOF(ntlm_config_rec, ntlm_basic_on),
+ OR_AUTHCFG,
+ "set to 'on' to allow Basic authentication too" ),
+
+ AP_INIT_TAKE1( "NTLMBasicRealm", ap_set_string_slot,
+ (void *) APR_OFFSETOF(ntlm_config_rec, ntlm_basic_realm),
+ OR_AUTHCFG, "realm to use for Basic authentication" ),
+
+#else
+ /* NTLM authentication commands */
+
+ { "NTLMAuth", ap_set_flag_slot,
+ (void *) XtOffsetOf(ntlm_config_rec, ntlm_on),
+ OR_AUTHCFG, FLAG, "set to 'on' to activate NTLM authentication" },
+
+ { "NegotiateAuth", ap_set_flag_slot,
+ (void *) XtOffsetOf(ntlm_config_rec, negotiate_on),
+ OR_AUTHCFG, FLAG, "set to 'on' to activate Negotiate authentication" },
+
+ { "NTLMBasicAuthoritative", ap_set_flag_slot,
+ (void *) XtOffsetOf(ntlm_config_rec, authoritative),
+ OR_AUTHCFG, FLAG,
+ "set to 'off' to allow access control to be passed along to lower "
+ "modules if the UserID is not known to this module" },
+
+ /* ntlm_auth location */
+
+ { "NTLMAuthHelper", ap_set_string_slot,
+ (void *) XtOffsetOf(ntlm_config_rec, ntlm_auth_helper), OR_AUTHCFG,
+ TAKE1, "location and arguments to the Samba ntlm_auth utility"},
+
+ { "NegotiateAuthHelper", ap_set_string_slot,
+ (void *) XtOffsetOf(ntlm_config_rec, negotiate_ntlm_auth_helper), OR_AUTHCFG,
+ TAKE1, "location and arguments to the Samba ntlm_auth utility"},
+
+ { "PlaintextAuthHelper", ap_set_string_slot,
+ (void *) XtOffsetOf(ntlm_config_rec, ntlm_plaintext_helper), OR_AUTHCFG,
+ TAKE1, "location and arguments to the Samba ntlm_auth utility"},
+
+ /* Basic Authentcation transport for non-IE browsers */
+
+ { "NTLMBasicAuth", ap_set_flag_slot,
+ (void *) XtOffsetOf(ntlm_config_rec, ntlm_basic_on),
+ OR_AUTHCFG, FLAG, "set to 'on' to allow Basic authentication too" },
+
+ { "NTLMBasicRealm", ap_set_string_slot,
+ (void *) XtOffsetOf(ntlm_config_rec, ntlm_basic_realm),
+ OR_AUTHCFG, TAKE1, "realm to use for Basic authentication" },
+#endif
+
+ { NULL }
+};
+
+/* both apache1 and apache2 use this to maintain per-child context */
+static ntlm_context_t global_ntlm_context;
+
+#ifndef APACHE2
+/* apache 1 doesn't seem to have a connection context I can use */
+static ntlm_connection_context_t global_connection_context;
+
+static void cleanup_ntlm_auth_helper(void *ntlm_conn_v)
+{
+ struct _ntlm_auth_helper *ntlm_conn = ntlm_conn_v;
+
+ ap_log_error( APLOG_MARK, NTLM_DEBUG, NULL, "freeing ntlm_helper" );
+
+ ap_bclose(ntlm_conn->out_to_helper);
+ ap_bclose(ntlm_conn->in_from_helper);
+
+ global_ntlm_context.ntlm_auth_helper = NULL;
+}
+
+static void cleanup_negotiate_ntlm_auth_helper(void *ntlm_conn_v)
+{
+ struct _ntlm_auth_helper *ntlm_conn = ntlm_conn_v;
+
+ ap_log_error( APLOG_MARK, NTLM_DEBUG, NULL, "freeing negotiate_helper" );
+
+ ap_bclose(ntlm_conn->out_to_helper);
+ ap_bclose(ntlm_conn->in_from_helper);
+
+ global_ntlm_context.negotiate_ntlm_auth_helper = NULL;
+}
+
+static void cleanup_ntlm_plaintext_helper( void *ntlm_conn_v ) {
+ struct _ntlm_auth_helper *ntlm_conn = ntlm_conn_v;
+
+ ap_log_error( APLOG_MARK, NTLM_DEBUG, NULL, "freeing plaintext_helper" );
+
+ ap_bclose( ntlm_conn->out_to_helper );
+ ap_bclose( ntlm_conn->in_from_helper );
+
+ global_ntlm_context.ntlm_plaintext_helper = NULL;
+}
+
+/* Dispose of a connected user */
+
+static void cleanup_connected_user_authenticated(void *unused)
+{
+ ap_log_error( APLOG_MARK, NTLM_DEBUG, NULL, "freeing user" );
+ global_connection_context.connected_user_authenticated = NULL;
+}
+#endif
+
+/* helper function to pull out the context data */
+static ntlm_connection_context_t *get_connection_context( struct conn_rec *connection ) {
+ ntlm_connection_context_t *retval = NULL;
+
+#ifdef APACHE2
+ retval = (ntlm_connection_context_t *)ap_get_module_config( connection->conn_config,
+ &auth_ntlm_winbind_module );
+#else
+ retval = &global_connection_context;
+#endif
+ return retval;
+}
+
+/* Authorisation has failed - we set some headers so the client can
+ get the hint and prompt for a password from the user. */
+
+static int
+note_auth_failure(request_rec * r, const char *negotiate_auth_line)
+{
+ ntlm_config_rec *crec
+ = (ntlm_config_rec *) ap_get_module_config(r->per_dir_config,
+ &auth_ntlm_winbind_module);
+ char *line;
+ ntlm_connection_context_t *ctxt = get_connection_context( r->connection );
+
+ /* MSIE will simply reply to the first, not strongest, protocol listed */
+ if (crec->negotiate_on) {
+ line = apr_pstrcat(r->pool, NEGOTIATE_AUTH_NAME, " ",
+ negotiate_auth_line, NULL);
+ apr_table_add(r->err_headers_out,
+ (PROXYREQ_PROXY == r->proxyreq) ? "Proxy-Authenticate" : "WWW-Authenticate",
+ line);
+ }
+
+ /* Set header for NTLM authentication */
+
+ if (crec->ntlm_on) {
+ apr_table_add(r->err_headers_out,
+ (PROXYREQ_PROXY == r->proxyreq) ? "Proxy-Authenticate" : "WWW-Authenticate",
+ NTLM_AUTH_NAME);
+ }
+
+ /* Set header for basic authentication so client can use this if
+ supported. */
+
+ if (crec->ntlm_basic_on) {
+ line = apr_pstrcat(r->pool,
+ "Basic realm=\"", crec->ntlm_basic_realm, "\"",
+ NULL);
+ apr_table_add(r->err_headers_out,
+ (PROXYREQ_PROXY == r->proxyreq) ? "Proxy-Authenticate" : "WWW-Authenticate",
+ line);
+ }
+
+ if ( ctxt->connected_user_authenticated &&
+ ctxt->connected_user_authenticated->pool ) {
+ apr_pool_destroy( ctxt->connected_user_authenticated->pool );
+ ctxt->connected_user_authenticated = NULL;
+ }
+
+ return HTTP_UNAUTHORIZED;
+}
+
+
+const char *
+get_auth_header(request_rec * r, ntlm_config_rec * crec, const char *auth_scheme)
+{
+ const char *auth_line = apr_table_get(r->headers_in,
+ (PROXYREQ_PROXY == r->proxyreq) ? "Proxy-Authorization"
+ : "Authorization");
+
+ if (!auth_line) {
+ RERROR( APR_EINIT, "no auth line present" );
+ return NULL;
+ }
+ if (strcmp(ap_getword_white(r->pool, &auth_line), auth_scheme)) {
+ RERROR( APR_EINIT, "%s auth name not present", auth_scheme );
+ return NULL;
+ }
+ return auth_line;
+}
+
+#ifndef APACHE2
+static int helper_child(void *child_stuff, child_info *pinfo)
+{
+ struct _ntlm_child_stuff *cld = (struct _ntlm_child_stuff *) child_stuff;
+ request_rec *r = cld->r;
+ char *argv0 = cld->argv0;
+ int child_pid;
+
+ RAISE_SIGSTOP(CGI_CHILD);
+
+ /* Transmute outselves into the helper
+ */
+
+ ap_cleanup_for_exec();
+
+ child_pid = ap_call_exec(r, pinfo, argv0, NULL, 1);
+
+ /* Uh oh. Still here. Where's the kaboom? There was supposed to be an
+ * EARTH-shattering kaboom!
+ *
+ * Oh, well. Muddle through as best we can...
+ *
+ * Note that only stderr is available at this point, so don't pass in
+ * a server to aplog_error.
+ */
+
+ RERROR( errno, "exec of %s failed", r->filename);
+ exit(0);
+ /* NOT REACHED */
+ return (0);
+}
+#endif
+
+
+static int
+send_auth_reply(request_rec * r, const char *auth_scheme, const char *reply)
+{
+ RDEBUG( "sending back %s", reply );
+ /* Read negotiate from ntlm_auth */
+
+ apr_table_setn(r->err_headers_out,
+ (PROXYREQ_PROXY == r->proxyreq) ? "Proxy-Authenticate" : "WWW-Authenticate",
+ apr_psprintf(r->pool, "%s %s", auth_scheme, reply));
+
+ /* This is to make sure that when receiving later messages
+ * that the r->connection is still alive after sending the
+ * nonce to the client. This code is written, and the problem
+ * was pointed out by Michael Cai.
+ */
+ if(r->connection->keepalives >= r->server->keep_alive_max)
+ {
+ RDEBUG("Decrement the connection request count to keep it alive");
+ r->connection->keepalives -= 1;
+ }
+
+ return HTTP_UNAUTHORIZED;
+}
+
+/* get the current request's auth helper or fork one */
+static struct _ntlm_auth_helper *get_auth_helper( request_rec *r, struct _ntlm_auth_helper *auth_helper, char *cmd, void (*cleanup)(void *)) {
+#ifdef APACHE2
+ apr_procattr_t *attr;
+#endif
+
+ if ( auth_helper == NULL ) {
+ struct _ntlm_child_stuff cld;
+ apr_pool_t *pool;
+#ifdef APACHE2
+ char **argv_out;
+ apr_pool_create_ex( &pool, NULL, NULL, NULL ); /* xxx return code */
+#else
+ pool = ap_make_sub_pool( NULL );
+#endif
+ auth_helper = apr_pcalloc( pool, sizeof( struct _ntlm_auth_helper ));
+ auth_helper->pool = pool;
+ auth_helper->helper_pid = 0;
+
+#ifdef APACHE2
+ apr_tokenize_to_argv( cmd, &argv_out, pool );
+#else
+ ap_register_cleanup( pool, auth_helper, cleanup, ap_null_cleanup );
+#endif
+ cld.argv0 = cmd;
+ cld.r = r;
+
+#ifdef APACHE2
+ apr_procattr_create( &attr, pool );
+ apr_procattr_io_set( attr, APR_FULL_BLOCK, APR_FULL_BLOCK, APR_NO_PIPE );
+ apr_procattr_error_check_set( attr, 1 );
+ auth_helper->proc = (apr_proc_t *)apr_pcalloc(pool, sizeof(apr_proc_t)) ;
+ if ( apr_proc_create( auth_helper->proc, argv_out[0], (const char * const *)argv_out, NULL, attr, pool ) != APR_SUCCESS ) {
+ RERROR( errno, "couldn't spawn child ntlm helper process: %s", argv_out[0]);
+ return NULL;
+ }
+ auth_helper->helper_pid = auth_helper->proc->pid;
+#else
+ auth_helper->helper_pid = ap_bspawn_child(pool, helper_child,
+ (void *) &cld, just_wait,
+ &auth_helper->out_to_helper,
+ &auth_helper->in_from_helper,
+ NULL);
+
+ if (auth_helper->helper_pid == -1) {
+ RERROR( errno, "couldn't spawn child ntlm helper process: %s", cld.argv0);
+ return NULL;
+ }
+#endif
+
+ RDEBUG( "Launched ntlm_helper, pid %d", auth_helper->helper_pid );
+ } else {
+ RDEBUG( "Using existing auth helper %d", auth_helper->helper_pid );
+ }
+
+ return auth_helper;
+}
+
+/* Call winbind to authenticate a (user, password)
+ pair */
+static int winbind_authenticate_plaintext( request_rec *r, ntlm_config_rec * crec, char *user, char *pass)
+{
+ ntlm_connection_context_t *ctxt = get_connection_context( r->connection );
+ char *newline;
+ char args_to_helper[HUGE_STRING_LEN];
+ char args_from_helper[HUGE_STRING_LEN];
+ size_t bytes_written;
+ int bytes_read;
+
+ if (( global_ntlm_context.ntlm_plaintext_helper = get_auth_helper( r, global_ntlm_context.ntlm_plaintext_helper, crec->ntlm_plaintext_helper, CLEANUP(cleanup_ntlm_plaintext_helper))) == NULL ) {
+ return HTTP_INTERNAL_SERVER_ERROR;
+ }
+
+ if ( ctxt->connected_user_authenticated == NULL ) {
+ apr_pool_t *pool;
+
+ RDEBUG( "creating auth user" );
+
+#ifdef APACHE2
+ apr_pool_create_ex( &pool, r->connection->pool, NULL, NULL );
+#else
+ pool = ap_make_sub_pool(r->connection->pool);
+#endif
+
+ ctxt->connected_user_authenticated =
+ apr_pcalloc(pool, sizeof( struct _connected_user_authenticated));
+
+#ifndef APACHE2
+ ap_register_cleanup(pool,ctxt->connected_user_authenticated,
+ cleanup_connected_user_authenticated, ap_null_cleanup );
+#endif
+
+ ctxt->connected_user_authenticated->pool = pool;
+ ctxt->connected_user_authenticated->user = NULL;
+ ctxt->connected_user_authenticated->auth_type = NULL;
+ } else {
+ /* what, we're already authenticated? */
+ return OK;
+ }
+
+ snprintf( args_to_helper, HUGE_STRING_LEN, "%s %s\n", user, pass );
+
+#ifdef APACHE2
+ bytes_written = strlen( args_to_helper );
+ apr_file_write( global_ntlm_context.ntlm_plaintext_helper->proc->in, args_to_helper, &bytes_written );
+#else
+ bytes_written = ap_bwrite( global_ntlm_context.ntlm_plaintext_helper->out_to_helper, args_to_helper, strlen( args_to_helper ));
+#endif
+
+ if ( bytes_written < strlen( args_to_helper )) {
+ RDEBUG( "failed to write user/pass to helper - wrote %d bytes", (int) bytes_written );
+ apr_pool_destroy( global_ntlm_context.ntlm_plaintext_helper->pool );
+ apr_pool_destroy( ctxt->connected_user_authenticated->pool );
+ return HTTP_INTERNAL_SERVER_ERROR;
+ }
+
+#ifdef APACHE2
+ apr_file_flush( global_ntlm_context.ntlm_plaintext_helper->proc->in );
+ if ( apr_file_gets( args_from_helper, HUGE_STRING_LEN, global_ntlm_context.ntlm_plaintext_helper->proc->out ) == APR_SUCCESS ) {
+ bytes_read = strlen( args_from_helper );
+ } else {
+ bytes_read = 0;
+ }
+#else
+ ap_bflush( global_ntlm_context.ntlm_plaintext_helper->out_to_helper );
+ bytes_read = ap_bgets( args_from_helper, HUGE_STRING_LEN, global_ntlm_context.ntlm_plaintext_helper->in_from_helper );
+#endif
+ if ( bytes_read == 0 ) {
+ RERROR( errno, "early EOF from helper" );
+ apr_pool_destroy(global_ntlm_context.ntlm_plaintext_helper->pool);
+ apr_pool_destroy(ctxt->connected_user_authenticated->pool);
+
+ return HTTP_INTERNAL_SERVER_ERROR;
+ } else if (bytes_read == -1) {
+ RERROR( errno, "helper died!" );
+ apr_pool_destroy(global_ntlm_context.ntlm_plaintext_helper->pool);
+ apr_pool_destroy(ctxt->connected_user_authenticated->pool);
+
+ return HTTP_INTERNAL_SERVER_ERROR;
+ } else if (bytes_read < 2) {
+ RERROR( errno, "failed to read NTLMSSP string from helper - only got %d bytes", bytes_read);
+ apr_pool_destroy(global_ntlm_context.ntlm_plaintext_helper->pool);
+ apr_pool_destroy(ctxt->connected_user_authenticated->pool);
+
+ return HTTP_INTERNAL_SERVER_ERROR;
+ }
+
+ newline = strchr(args_from_helper, '\n');
+ if (newline != NULL) {
+ *newline = '\0';
+ }
+
+ RDEBUG( "got response: %s", args_from_helper );
+
+ if ( strncmp( args_from_helper, "OK", 2 ) == 0 ) {
+ RDEBUG( "authentication succeeded!" );
+ ctxt->connected_user_authenticated->user = apr_pstrdup(ctxt->connected_user_authenticated->pool, user);
+ ctxt->connected_user_authenticated->keepalives = r->connection->keepalives;
+#ifdef APACHE2
+ r->user = ctxt->connected_user_authenticated->user;
+ r->ap_auth_type = apr_pstrdup(r->connection->pool, "Basic");
+ /* disconnect the child process */
+ /* apr_proc_kill( global_ntlm_context.ntlm_plaintext_helper->proc, 9 );
+ apr_proc_wait( global_ntlm_context.ntlm_plaintext_helper->proc, &exit, &why, APR_WAIT );*/
+#else
+ r->connection->user = ctxt->connected_user_authenticated->user;
+ r->connection->ap_auth_type = ap_pstrdup(r->connection->pool, "Basic");
+#endif
+ RDEBUG( "authenticated %s", ctxt->connected_user_authenticated->user );
+ return OK;
+ } else {
+ if ( strncmp( args_from_helper, "ERR", 3 ) == 0 ) {
+ RDEBUG( "username/password incorrect" );
+ return note_auth_failure( r, NULL );
+ } else {
+ RDEBUG( "unknown helper response %s", args_from_helper );
+ return HTTP_INTERNAL_SERVER_ERROR;
+ }
+ }
+}
+
+/* Process a message received from the client. This can be a request for a
+ challenge (type 1) or a request to authenticate a challenge/response
+ (type 3). */
+
+static int
+process_msg(request_rec * r, ntlm_config_rec * crec, const char *auth_type)
+{
+ const char *client_msg;
+ const char *message_type;
+ char *childarg;
+ char *newline;
+ char args_to_helper[HUGE_STRING_LEN];
+ char args_from_helper[HUGE_STRING_LEN];
+ ntlm_connection_context_t *ctxt = get_connection_context( r->connection );
+ size_t bytes_written;
+ int bytes_read;
+ struct _ntlm_auth_helper *auth_helper;
+
+ /* If this is the first request with this connection, then create
+ * a ntlm_auth_helper entry for it. It will be cleaned up when the
+ * connection is dropped */
+
+ if (strcmp(auth_type, NEGOTIATE_AUTH_NAME) == 0) {
+ auth_helper = get_auth_helper( r, global_ntlm_context.negotiate_ntlm_auth_helper, crec->negotiate_ntlm_auth_helper, CLEANUP(cleanup_negotiate_ntlm_auth_helper));
+ global_ntlm_context.negotiate_ntlm_auth_helper = auth_helper;
+ } else if (strcmp(auth_type, NTLM_AUTH_NAME) == 0) {
+ auth_helper = get_auth_helper( r, global_ntlm_context.ntlm_auth_helper, crec->ntlm_auth_helper, CLEANUP(cleanup_ntlm_auth_helper));
+ global_ntlm_context.ntlm_auth_helper = auth_helper;
+ } else {
+ auth_helper = NULL;
+ }
+
+ if ( auth_helper == NULL ) {
+ return HTTP_INTERNAL_SERVER_ERROR;
+ }
+
+ if ( ctxt->connected_user_authenticated == NULL ) {
+ apr_pool_t *pool;
+
+ RDEBUG( "creating auth user" );
+
+#ifdef APACHE2
+ apr_pool_create_ex( &pool, r->connection->pool, NULL, NULL );
+#else
+ pool = ap_make_sub_pool(r->connection->pool);
+#endif
+
+ ctxt->connected_user_authenticated =
+ apr_pcalloc(pool, sizeof( struct _connected_user_authenticated));
+
+#ifndef APACHE2
+ ap_register_cleanup(pool,ctxt->connected_user_authenticated,
+ cleanup_connected_user_authenticated, ap_null_cleanup );
+#endif
+
+ ctxt->connected_user_authenticated->pool = pool;
+ ctxt->connected_user_authenticated->user = NULL;
+ ctxt->connected_user_authenticated->auth_type = NULL;
+
+ message_type = "YR";
+ } else {
+ message_type = "KK";
+ }
+
+ /* Decode the information the WWW-Authenticate header */
+ if ((client_msg = get_auth_header(r, crec, auth_type)) == NULL) {
+ RDEBUG( "client did not return NTLM authentication header");
+ return note_auth_failure(r, NULL);
+ }
+
+ /* Pipe to helper */
+ snprintf(args_to_helper, HUGE_STRING_LEN, "%s %s\n", message_type, client_msg);
+
+#ifdef APACHE2
+ bytes_written = strlen( args_to_helper );
+ apr_file_write( auth_helper->proc->in, args_to_helper, &bytes_written );
+#else
+ bytes_written = ap_bwrite(auth_helper->out_to_helper, args_to_helper, strlen(args_to_helper));
+#endif
+ if (bytes_written < strlen(args_to_helper)) {
+ RDEBUG("failed to write NTLMSSP string to helper - wrote %d bytes", (int) bytes_written);
+ apr_pool_destroy(auth_helper->pool);
+ apr_pool_destroy(ctxt->connected_user_authenticated->pool);
+
+ return HTTP_INTERNAL_SERVER_ERROR;
+ }
+
+#ifdef APACHE2
+ apr_file_flush( auth_helper->proc->in );
+
+ RDEBUG( "parsing reply from helper to %s", args_to_helper );
+
+ if ( apr_file_gets(args_from_helper, HUGE_STRING_LEN, auth_helper->proc->out ) == APR_SUCCESS ) {
+ bytes_read = strlen( args_from_helper );
+ } else {
+ bytes_read = 0;
+ }
+#else
+ ap_bflush(auth_helper->out_to_helper);
+
+ bytes_read = ap_bgets(args_from_helper, HUGE_STRING_LEN, auth_helper->in_from_helper);
+#endif
+
+ if (bytes_read == 0) {
+ RERROR( errno, "early EOF from helper" );
+ apr_pool_destroy(auth_helper->pool);
+ apr_pool_destroy(ctxt->connected_user_authenticated->pool);
+
+ return HTTP_INTERNAL_SERVER_ERROR;
+ } else if (bytes_read == -1) {
+ RERROR( errno, "helper died!");
+ apr_pool_destroy(auth_helper->pool);
+ apr_pool_destroy(ctxt->connected_user_authenticated->pool);
+
+ return HTTP_INTERNAL_SERVER_ERROR;
+ } else if (bytes_read < 2) {
+ RERROR( errno, "failed to read NTLMSSP string from helper - only got %d bytes", bytes_read );
+ apr_pool_destroy(auth_helper->pool);
+ apr_pool_destroy(ctxt->connected_user_authenticated->pool);
+
+ return HTTP_INTERNAL_SERVER_ERROR;
+ }
+
+ newline = strchr(args_from_helper, '\n');
+ if (newline != NULL) {
+ *newline = '\0';
+ }
+
+ RDEBUG( "got response: %s", args_from_helper );
+
+ /* inspect message type */
+
+ childarg = strchr(args_from_helper, ' ');
+ if (childarg == NULL) {
+ RERROR( errno, "failed to parse response from helper");
+ apr_pool_destroy(auth_helper->pool);
+ apr_pool_destroy(ctxt->connected_user_authenticated->pool);
+
+ return HTTP_INTERNAL_SERVER_ERROR;
+ }
+ childarg++;
+
+ if (strcasecmp(auth_type, NTLM_AUTH_NAME) == 0) {
+ /* if TT, send to client */
+
+ if (strncmp(args_from_helper, "TT ", 3) == 0) {
+ return send_auth_reply(r, auth_type, childarg);
+ }
+
+ /* if NA, not authenticated */
+
+ if (strncmp(args_from_helper, "NA ", 3) == 0) {
+ RDEBUG("user not authenticated: %s", childarg);
+ return note_auth_failure(r, NULL);
+ }
+
+ /* if AF, record username */
+ if (strncmp(args_from_helper, "AF ", 3) == 0) {
+ ctxt->connected_user_authenticated->user =
+ apr_pstrdup(ctxt->connected_user_authenticated->pool,
+ childarg);
+ ctxt->connected_user_authenticated->keepalives =
+ r->connection->keepalives;
+#ifdef APACHE2
+ r->user = ctxt->connected_user_authenticated->user;
+ r->ap_auth_type = apr_pstrdup(r->connection->pool, auth_type);
+ /* disconnect the child process */
+ /* apr_proc_kill( auth_helper->proc, 9 );
+ apr_proc_wait( auth_helper->proc, &exit, &why, APR_WAIT );*/
+#else
+ r->connection->user = ctxt->connected_user_authenticated->user;
+ r->connection->ap_auth_type = ap_pstrdup(r->connection->pool, auth_type);
+#endif
+ RDEBUG( "authenticated %s",
+ ctxt->connected_user_authenticated->user );
+ return OK;
+ }
+ } else if (strcasecmp(auth_type, NEGOTIATE_AUTH_NAME) == 0) {
+
+ /* The child's reply contains 3 parts:
+ - The code: TT, AF or NA
+ - The blob to send to the client, coded in base64
+ - The argument:
+ For TT it's a dummy '*'
+ For AF it's domain\\user
+ For NA it's the NT error code
+ */
+
+ char *childarg3 = strchr(childarg, ' ');
+ if (childarg3 == NULL) {
+ RERROR( errno, "failed to parse response from helper");
+ apr_pool_destroy(auth_helper->pool);
+ apr_pool_destroy(ctxt->connected_user_authenticated->pool);
+
+ return HTTP_INTERNAL_SERVER_ERROR;
+ }
+ *childarg3 = '\0';
+ childarg3++;
+
+ /* if TT, send to client */
+
+ if (strncmp(args_from_helper, "TT ", 3) == 0) {
+ return send_auth_reply(r, auth_type, childarg);
+ }
+
+ /* if NA, not authenticated */
+
+ if (strncmp(args_from_helper, "NA ", 3) == 0) {
+ RDEBUG("user not authenticated: %s", childarg3);
+ return note_auth_failure(r, childarg);
+ }
+
+ /* if AF, record username */
+ if (strncmp(args_from_helper, "AF ", 3) == 0) {
+ ctxt->connected_user_authenticated->user =
+ apr_pstrdup(ctxt->connected_user_authenticated->pool,
+ childarg3);
+#ifdef APACHE2
+ r->user = ctxt->connected_user_authenticated->user;
+ ctxt->connected_user_authenticated->auth_type =
+ apr_pstrdup(r->connection->pool, auth_type);
+ r->ap_auth_type = ctxt->connected_user_authenticated->auth_type;
+#else
+ r->connection->user = ctxt->connected_user_authenticated->user;
+ ctxt->connected_user_authenticated->auth_type = ap_pstrdup(r->connection->pool, auth_type);
+ r->connection->ap_auth_type = ctxt->connected_user_authenticated->auth_type;
+#endif
+
+ if (strcmp("*", childarg) != 0) {
+ /* Send last leg (possible mutual authentication token) */
+ apr_table_setn(r->headers_out,
+ (PROXYREQ_PROXY == r->proxyreq) ? "Proxy-Authenticate" : "WWW-Authenticate",
+ apr_psprintf(r->pool, "%s %s", auth_type, childarg));
+ }
+
+ RDEBUG( "wow, we're all happy here" );
+
+ return OK;
+ }
+ }
+
+ /* Helper failed */
+
+ /* if BH, helper is busted */
+
+ if (strncmp(args_from_helper, "BH ", 3) == 0) {
+ RERROR( APR_EGENERAL, "ntlm_auth reports Broken Helper: %s", args_from_helper);
+ } else {
+ RERROR( APR_EGENERAL, "could not parse %s helper callback: %s", auth_type, args_from_helper);
+ }
+
+ apr_pool_destroy(auth_helper->pool);
+ apr_pool_destroy(ctxt->connected_user_authenticated->pool);
+
+ return HTTP_INTERNAL_SERVER_ERROR;
+}
+
+/* Called to create a configuration structure for each section
+ that uses the ntlm auth module. */
+
+static void *
+ntlm_winbind_dir_config(apr_pool_t * p, char *d)
+{
+ ntlm_config_rec *crec
+ = (ntlm_config_rec *) apr_pcalloc(p, sizeof(ntlm_config_rec));
+
+ /* Set the defaults. */
+
+ crec->authoritative = 1;
+ crec->ntlm_on = 0;
+ crec->negotiate_on = 0;
+ crec->ntlm_basic_on = 0;
+ crec->ntlm_basic_realm = "REALM";
+ crec->ntlm_auth_helper = "ntlm_auth --helper-protocol=squid-2.5-ntlmssp";
+ crec->negotiate_ntlm_auth_helper = "ntlm_auth --helper-protocol=gss-spnego";
+ crec->ntlm_plaintext_helper = "ntlm_auth --helper-protocol=squid-2.5-basic";
+
+ return crec;
+}
+
+/* Authenticate a user using basic authentication */
+static int
+authenticate_basic_user(request_rec * r, ntlm_config_rec * crec,
+ const char *auth_line_after_Basic)
+{
+ char *sent_user = NULL, *sent_pw;
+ int result = HTTP_UNAUTHORIZED;
+
+ while (*auth_line_after_Basic == ' ' || *auth_line_after_Basic == '\t')
+ auth_line_after_Basic++;
+
+#ifdef APACHE2
+ sent_user = apr_pcalloc( r->pool, apr_base64_decode_len( auth_line_after_Basic ));
+ apr_base64_decode( sent_user, auth_line_after_Basic );
+#else
+ sent_user = ap_pbase64decode(r->pool, auth_line_after_Basic);
+#endif
+
+ if (sent_user != NULL) {
+ char *s;
+
+ if ((sent_pw = strchr(sent_user, ':')) != NULL) {
+ *sent_pw = '\0';
+ ++sent_pw;
+ } else
+ sent_pw = "";
+
+ result = winbind_authenticate_plaintext( r, crec, sent_user, sent_pw);
+ } else {
+ RDEBUG("can't extract user from %s", auth_line_after_Basic );
+ sent_user = sent_pw = "";
+ }
+
+ RDEBUG("authenticate user %s: %s", sent_user,
+ (result == OK) ? "OK" : "FAILED");
+ return result;
+}
+
+/* Check the user id from a http request */
+static int check_user_id(request_rec * r) {
+ ntlm_config_rec *crec =
+ (ntlm_config_rec *) ap_get_module_config(r->per_dir_config,
+ &auth_ntlm_winbind_module);
+ ntlm_connection_context_t *ctxt = get_connection_context( r->connection );
+ const char *auth_line = apr_table_get(r->headers_in,
+ (PROXYREQ_PROXY == r->proxyreq) ? "Proxy-Authorization"
+ : "Authorization");
+ const char *auth_line2;
+
+ /* Trust the authentication on an existing connection */
+ if (ctxt->connected_user_authenticated && ctxt->connected_user_authenticated->user) {
+ /* internal redirects cause this to get called more than once
+ per request on Apache 1.x. This compensates by checking if
+ the connection is the same as the one we authed against */
+ if ( !auth_line || ( ctxt->connected_user_authenticated->keepalives == r->connection->keepalives )) {
+ /* silently accept login with same credentials */
+ RDEBUG( "retaining user %s",
+ ctxt->connected_user_authenticated->user );
+ RDEBUG( "keepalives: %d", r->connection->keepalives );
+#ifdef APACHE2
+ r->user = ctxt->connected_user_authenticated->user;
+ r->ap_auth_type = ctxt->connected_user_authenticated->auth_type;
+#else
+ r->connection->user = ctxt->connected_user_authenticated->user;
+ r->connection->ap_auth_type = ctxt->connected_user_authenticated->auth_type;
+#endif
+ return OK;
+ } else {
+ RDEBUG( "reauth" );
+ /* client wishes to re-authenticate this TCP socket */
+ if ( ctxt->connected_user_authenticated->pool ) {
+ apr_pool_destroy(ctxt->connected_user_authenticated->pool);
+ ctxt->connected_user_authenticated = NULL;
+ }
+ }
+ }
+
+ /* No authentication line given. Return a 401 and a WWW-Authenticate
+ header so authentication can commence. */
+
+ if (!auth_line) {
+ note_auth_failure(r, NULL);
+ return HTTP_UNAUTHORIZED;
+ }
+
+ /* If basic authentication is requested and enabled, try to
+ authenticate the user with basic */
+
+ auth_line2 = auth_line;
+ if (crec->ntlm_basic_on
+ && strcasecmp(ap_getword(r->pool, &auth_line2, ' '), "Basic") == 0) {
+ RDEBUG( "trying basic auth" );
+ return authenticate_basic_user(r, crec, &auth_line[5]); /* seems clunky */
+ }
+
+ /* Process a 'Negotiate' SPNEGO over http message */
+ auth_line2 = auth_line;
+ if (strcasecmp(ap_getword(r->pool, &auth_line2, ' '), NEGOTIATE_AUTH_NAME) == 0) {
+ if (!crec->negotiate_on) {
+ RDEBUG("Negotiate authentication is not enabled");
+ return DECLINED;
+ } else {
+ return process_msg(r, crec, NEGOTIATE_AUTH_NAME);
+ }
+ }
+
+ /* Process a NTLM over http message */
+
+ auth_line2 = auth_line;
+ if (strcasecmp(ap_getword(r->pool, &auth_line2, ' '), NTLM_AUTH_NAME) == 0) {
+ if (!crec->ntlm_on) {
+ RDEBUG("NTLM authentication is not enabled");
+ return DECLINED;
+ } else {
+ RDEBUG( "doing ntlm auth dance" );
+ return process_msg(r, crec, NTLM_AUTH_NAME);
+ }
+ }
+
+ if (ctxt->connected_user_authenticated && ctxt->connected_user_authenticated->pool ) {
+ apr_pool_destroy(ctxt->connected_user_authenticated->pool);
+ ctxt->connected_user_authenticated = NULL;
+ }
+
+ RDEBUG( "declined" );
+
+ return DECLINED;
+}
+
+/* Dispatch list for API hooks */
+#ifdef APACHE2
+static int ntlm_pre_conn(conn_rec *c, void *csd) {
+ ntlm_connection_context_t *ctxt = apr_pcalloc(c->pool, sizeof(ntlm_connection_context_t));
+
+ ap_set_module_config(c->conn_config, &auth_ntlm_winbind_module, ctxt);
+
+ return OK;
+}
+
+
+static void register_hooks(apr_pool_t *pool)
+{
+ ap_hook_pre_connection(ntlm_pre_conn,NULL,NULL,APR_HOOK_MIDDLE);
+ ap_hook_check_user_id(check_user_id,NULL,NULL,APR_HOOK_MIDDLE);
+};
+
+module AP_MODULE_DECLARE_DATA auth_ntlm_winbind_module = {
+ STANDARD20_MODULE_STUFF,
+ ntlm_winbind_dir_config, /* create per-dir config structures */
+ NULL, /* merge per-dir config structures */
+ NULL, /* create per-server config structures */
+ NULL, /* merge per-server config structures */
+ ntlm_winbind_cmds, /* table of config file commands */
+ register_hooks, /* register hooks */
+};
+#else
+module MODULE_VAR_EXPORT auth_ntlm_winbind_module = {
+ STANDARD_MODULE_STUFF,
+ NULL, /* module initializer */
+ ntlm_winbind_dir_config, /* create per-dir config structures */
+ NULL, /* merge per-dir config structures */
+ NULL, /* create per-server config structures */
+ NULL, /* merge per-server config structures */
+ ntlm_winbind_cmds, /* table of config file commands */
+ NULL, /* [#8] MIME-typed-dispatched handlers */
+ NULL, /* [#1] URI to filename translation */
+ check_user_id, /* [#4] validate user id from request */
+ NULL, /* [#5] check if the user is ok _here_ */
+ NULL, /* [#3] check access by host address */
+ NULL, /* [#6] determine MIME type */
+ NULL, /* [#7] pre-run fixups */
+ NULL, /* [#9] log a transaction */
+ NULL, /* [#2] header parser */
+ NULL, /* child_init */
+ NULL, /* child_exit */
+ NULL /* [#0] post read-request */
+#ifdef EAPI
+ ,NULL, /* EAPI: add_module */
+ NULL, /* EAPI: remove_module */
+ NULL, /* EAPI: rewrite_command */
+ NULL /* EAPI: new_connection */
+#endif
+};
+#endif
+
+/*
+ * Local Variables:
+ * c-basic-offset: 4
+ * indent-tabs-mode: nil
+ * End:
+ */
diff --git a/src/libraries/Common/tests/System/Net/EnterpriseTests/setup/apacheweb/proftpd.conf b/src/libraries/Common/tests/System/Net/EnterpriseTests/setup/apacheweb/proftpd.conf
new file mode 100644
index 00000000000000..7d9a20fd8f22df
--- /dev/null
+++ b/src/libraries/Common/tests/System/Net/EnterpriseTests/setup/apacheweb/proftpd.conf
@@ -0,0 +1,46 @@
+# Basic ProFTPD configuration for enterprise testing
+ServerName "ProFTPD Test Server"
+ServerType standalone
+DefaultServer on
+Port 21
+Umask 022
+MaxInstances 30
+
+# Load TLS module
+LoadModule mod_tls.c
+
+# Set the directory for user home
+DefaultRoot ~
+
+# TLS/SSL Configuration
+
+TLSEngine on
+TLSLog /var/log/proftpd/tls.log
+TLSProtocol TLSv1.2 TLSv1.3
+TLSRequired off
+TLSRSACertificateFile /etc/proftpd/ssl/proftpd.crt
+TLSRSACertificateKeyFile /etc/proftpd/ssl/proftpd.key
+TLSVerifyClient off
+
+# Support for explicit FTPS (AUTH TLS)
+TLSOptions NoSessionReuseRequired
+
+# No TLS renegotiation
+TLSRenegotiate none
+
+
+# Passive mode configuration
+PassivePorts 50000 50100
+
+# Allow overwrite
+AllowOverwrite on
+
+# Logging
+TransferLog /var/log/proftpd/xferlog
+SystemLog /var/log/proftpd/proftpd.log
+
+
+
+ AllowUser ftpuser
+
+
diff --git a/src/libraries/Common/tests/System/Net/EnterpriseTests/setup/apacheweb/run.sh b/src/libraries/Common/tests/System/Net/EnterpriseTests/setup/apacheweb/run.sh
index 8fd72c5f2c7204..9800a7f26e5218 100644
--- a/src/libraries/Common/tests/System/Net/EnterpriseTests/setup/apacheweb/run.sh
+++ b/src/libraries/Common/tests/System/Net/EnterpriseTests/setup/apacheweb/run.sh
@@ -19,4 +19,15 @@ fi
./setup-digest.sh
+# Start ProFTPD in the background for FTP/SSL testing
+echo "Starting ProFTPD..."
+/usr/sbin/proftpd
+sleep 1
+
+# Check if ProFTPD is running
+if ! pgrep -x proftpd > /dev/null; then
+ echo "ProFTPD failed to start, checking logs..."
+ cat /var/log/proftpd/proftpd.log 2>/dev/null || echo "No ProFTPD log found"
+fi
+
exec /usr/sbin/apache2 -DFOREGROUND "$@"
diff --git a/src/libraries/Common/tests/System/Net/EnterpriseTests/setup/kdc/Dockerfile b/src/libraries/Common/tests/System/Net/EnterpriseTests/setup/kdc/Dockerfile
index 3bfc206c5f0e22..43479f23df7e63 100644
--- a/src/libraries/Common/tests/System/Net/EnterpriseTests/setup/kdc/Dockerfile
+++ b/src/libraries/Common/tests/System/Net/EnterpriseTests/setup/kdc/Dockerfile
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-18.04
+FROM mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-24.04
COPY ./kdc/kadm5.acl /etc/krb5kdc/
COPY ./kdc/kdc.conf /etc/krb5kdc/
diff --git a/src/libraries/Common/tests/System/Net/EnterpriseTests/setup/linuxclient/Dockerfile b/src/libraries/Common/tests/System/Net/EnterpriseTests/setup/linuxclient/Dockerfile
index 67f5cf0aa617bd..9135188a3bb6b5 100644
--- a/src/libraries/Common/tests/System/Net/EnterpriseTests/setup/linuxclient/Dockerfile
+++ b/src/libraries/Common/tests/System/Net/EnterpriseTests/setup/linuxclient/Dockerfile
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-22.04
+FROM mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-24.04
# Prevents dialog prompting when installing packages
ARG DEBIAN_FRONTEND=noninteractive
diff --git a/src/libraries/System.Net.Requests/src/System/Net/FtpDataStream.cs b/src/libraries/System.Net.Requests/src/System/Net/FtpDataStream.cs
index 129052a9713076..b67ff89b919c1e 100644
--- a/src/libraries/System.Net.Requests/src/System/Net/FtpDataStream.cs
+++ b/src/libraries/System.Net.Requests/src/System/Net/FtpDataStream.cs
@@ -77,10 +77,32 @@ void ICloseEx.CloseEx(CloseExState closeState)
{
try
{
- if ((closeState & CloseExState.Abort) == 0)
- _originalStream.Close(DefaultCloseTimeout);
+ // If we have an SslStream wrapping the NetworkStream, close it first.
+ // The SslStream will handle proper SSL/TLS shutdown and close the underlying NetworkStream.
+ if (_stream != _originalStream)
+ {
+ // Close the SslStream with appropriate timeout
+ if ((closeState & CloseExState.Abort) == 0)
+ {
+ // For normal close, use Close() which sends TLS close_notify
+ // The SslStream will close the underlying NetworkStream
+ _stream.Close();
+ }
+ else
+ {
+ // For abort, just dispose without graceful shutdown
+ _stream.Dispose();
+ _originalStream.Close(0);
+ }
+ }
else
- _originalStream.Close(0);
+ {
+ // No SslStream wrapping, close the NetworkStream directly with timeout
+ if ((closeState & CloseExState.Abort) == 0)
+ _originalStream.Close(DefaultCloseTimeout);
+ else
+ _originalStream.Close(0);
+ }
}
finally
{
diff --git a/src/libraries/System.Net.Requests/tests/EnterpriseTests/FtpWebRequestAuthenticationTest.cs b/src/libraries/System.Net.Requests/tests/EnterpriseTests/FtpWebRequestAuthenticationTest.cs
new file mode 100644
index 00000000000000..25fbcbfc279a35
--- /dev/null
+++ b/src/libraries/System.Net.Requests/tests/EnterpriseTests/FtpWebRequestAuthenticationTest.cs
@@ -0,0 +1,253 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.IO;
+using System.Net;
+using System.Net.Security;
+using System.Net.Test.Common;
+using System.Security.Cryptography.X509Certificates;
+using System.Text;
+using System.Threading.Tasks;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace System.Net.Tests
+{
+ [ConditionalClass(typeof(EnterpriseTestConfiguration), nameof(EnterpriseTestConfiguration.Enabled))]
+ public class FtpWebRequestAuthenticationTest
+ {
+ private readonly ITestOutputHelper _output;
+ private const string FtpServerUrl = "apacheweb.linux.contoso.com";
+ private const string FtpUsername = "ftpuser";
+ private const string FtpPassword = "ftppass";
+
+ public FtpWebRequestAuthenticationTest(ITestOutputHelper output)
+ {
+ _output = output;
+
+ // Set up certificate validation callback to accept self-signed certificates in test environment
+#pragma warning disable SYSLIB0014 // ServicePointManager is obsolete
+ ServicePointManager.ServerCertificateValidationCallback = ValidateServerCertificate;
+#pragma warning restore SYSLIB0014
+ }
+
+ private static bool ValidateServerCertificate(
+ object sender,
+ X509Certificate? certificate,
+ X509Chain? chain,
+ SslPolicyErrors sslPolicyErrors)
+ {
+ // In the enterprise test environment, accept self-signed certificates
+ // This is safe because we're in a controlled test environment
+ return true;
+ }
+
+ [ConditionalFact(typeof(EnterpriseTestConfiguration), nameof(EnterpriseTestConfiguration.Enabled))]
+#pragma warning disable SYSLIB0014 // WebRequest, FtpWebRequest, and related types are obsolete
+ public void FtpUpload_NoSsl_Baseline()
+ {
+ string fileName = $"test_{Guid.NewGuid()}.txt";
+ string url = $"ftp://{FtpUsername}:{FtpPassword}@{FtpServerUrl}/ftp/{fileName}";
+ byte[] data = Encoding.UTF8.GetBytes("Test data for FTP upload without SSL");
+
+ _output.WriteLine($"Testing baseline FTP upload without SSL to: {url}");
+
+ // Upload file without SSL (baseline test)
+ FtpWebRequest request = (FtpWebRequest)WebRequest.Create(url);
+ request.Method = WebRequestMethods.Ftp.UploadFile;
+ request.EnableSsl = false;
+ request.Credentials = new NetworkCredential(FtpUsername, FtpPassword);
+
+ using (var stream = request.GetRequestStream())
+ {
+ stream.Write(data, 0, data.Length);
+ }
+
+ using (FtpWebResponse response = (FtpWebResponse)request.GetResponse())
+ {
+ _output.WriteLine($"Response: {response.StatusCode} - {response.StatusDescription}");
+ Assert.Equal(FtpStatusCode.ClosingData, response.StatusCode);
+ }
+
+ // Cleanup
+ try
+ {
+ FtpWebRequest deleteRequest = (FtpWebRequest)WebRequest.Create(url);
+ deleteRequest.Method = WebRequestMethods.Ftp.DeleteFile;
+ deleteRequest.EnableSsl = false;
+ deleteRequest.Credentials = new NetworkCredential(FtpUsername, FtpPassword);
+ using (FtpWebResponse deleteResponse = (FtpWebResponse)deleteRequest.GetResponse())
+ {
+ _output.WriteLine($"Delete response: {deleteResponse.StatusCode}");
+ }
+ }
+ catch (Exception ex)
+ {
+ _output.WriteLine($"Cleanup failed: {ex.Message}");
+ }
+ }
+#pragma warning restore SYSLIB0014
+
+ [ConditionalFact(typeof(EnterpriseTestConfiguration), nameof(EnterpriseTestConfiguration.Enabled))]
+#pragma warning disable SYSLIB0014 // WebRequest, FtpWebRequest, and related types are obsolete
+ public async Task FtpUploadWithSsl_Success()
+ {
+ string fileName = $"test_{Guid.NewGuid()}.txt";
+ string url = $"ftp://{FtpUsername}:{FtpPassword}@{FtpServerUrl}/ftp/{fileName}";
+ byte[] data = Encoding.UTF8.GetBytes("Test data for FTP/SSL upload");
+
+ _output.WriteLine($"Testing FTP upload with SSL to: {url}");
+
+ // Upload file with SSL
+ FtpWebRequest uploadRequest = (FtpWebRequest)WebRequest.Create(url);
+ uploadRequest.Method = WebRequestMethods.Ftp.UploadFile;
+ uploadRequest.EnableSsl = true;
+ uploadRequest.Credentials = new NetworkCredential(FtpUsername, FtpPassword);
+
+ using (Stream requestStream = await uploadRequest.GetRequestStreamAsync())
+ {
+ await requestStream.WriteAsync(data, 0, data.Length);
+ }
+
+ using (FtpWebResponse response = (FtpWebResponse)await uploadRequest.GetResponseAsync())
+ {
+ _output.WriteLine($"Upload response: {response.StatusCode} - {response.StatusDescription}");
+ Assert.Equal(FtpStatusCode.ClosingData, response.StatusCode);
+ }
+
+ // Cleanup - delete the uploaded file
+ try
+ {
+ FtpWebRequest deleteRequest = (FtpWebRequest)WebRequest.Create(url);
+ deleteRequest.Method = WebRequestMethods.Ftp.DeleteFile;
+ deleteRequest.EnableSsl = true;
+ deleteRequest.Credentials = new NetworkCredential(FtpUsername, FtpPassword);
+ using (FtpWebResponse deleteResponse = (FtpWebResponse)await deleteRequest.GetResponseAsync())
+ {
+ _output.WriteLine($"Delete response: {deleteResponse.StatusCode}");
+ }
+ }
+ catch (Exception ex)
+ {
+ _output.WriteLine($"Cleanup failed: {ex.Message}");
+ }
+ }
+#pragma warning restore SYSLIB0014
+
+ [ConditionalFact(typeof(EnterpriseTestConfiguration), nameof(EnterpriseTestConfiguration.Enabled))]
+#pragma warning disable SYSLIB0014 // WebRequest, FtpWebRequest, and related types are obsolete
+ public async Task FtpDownloadWithSsl_Success()
+ {
+ string fileName = $"test_{Guid.NewGuid()}.txt";
+ string url = $"ftp://{FtpUsername}:{FtpPassword}@{FtpServerUrl}/ftp/{fileName}";
+ byte[] uploadData = Encoding.UTF8.GetBytes("Test data for FTP/SSL download");
+
+ _output.WriteLine($"Testing FTP download with SSL from: {url}");
+
+ // First upload a file
+ FtpWebRequest uploadRequest = (FtpWebRequest)WebRequest.Create(url);
+ uploadRequest.Method = WebRequestMethods.Ftp.UploadFile;
+ uploadRequest.EnableSsl = true;
+ uploadRequest.Credentials = new NetworkCredential(FtpUsername, FtpPassword);
+
+ using (Stream requestStream = await uploadRequest.GetRequestStreamAsync())
+ {
+ await requestStream.WriteAsync(uploadData, 0, uploadData.Length);
+ }
+
+ using (FtpWebResponse uploadResponse = (FtpWebResponse)await uploadRequest.GetResponseAsync())
+ {
+ _output.WriteLine($"Upload response: {uploadResponse.StatusCode}");
+ }
+
+ // Now download it
+ FtpWebRequest downloadRequest = (FtpWebRequest)WebRequest.Create(url);
+ downloadRequest.Method = WebRequestMethods.Ftp.DownloadFile;
+ downloadRequest.EnableSsl = true;
+ downloadRequest.Credentials = new NetworkCredential(FtpUsername, FtpPassword);
+
+ using (FtpWebResponse response = (FtpWebResponse)await downloadRequest.GetResponseAsync())
+ using (Stream responseStream = response.GetResponseStream())
+ using (MemoryStream ms = new MemoryStream())
+ {
+ await responseStream.CopyToAsync(ms);
+ byte[] downloadedData = ms.ToArray();
+ string downloadedText = Encoding.UTF8.GetString(downloadedData);
+
+ _output.WriteLine($"Downloaded {downloadedData.Length} bytes");
+ _output.WriteLine($"Download response: {response.StatusCode} - {response.StatusDescription}");
+
+ Assert.Equal(FtpStatusCode.ClosingData, response.StatusCode);
+ Assert.Equal(uploadData.Length, downloadedData.Length);
+ Assert.Equal(Encoding.UTF8.GetString(uploadData), downloadedText);
+ }
+
+ // Cleanup - delete the uploaded file
+ try
+ {
+ FtpWebRequest deleteRequest = (FtpWebRequest)WebRequest.Create(url);
+ deleteRequest.Method = WebRequestMethods.Ftp.DeleteFile;
+ deleteRequest.EnableSsl = true;
+ deleteRequest.Credentials = new NetworkCredential(FtpUsername, FtpPassword);
+ using (FtpWebResponse deleteResponse = (FtpWebResponse)await deleteRequest.GetResponseAsync())
+ {
+ _output.WriteLine($"Delete response: {deleteResponse.StatusCode}");
+ }
+ }
+ catch (Exception ex)
+ {
+ _output.WriteLine($"Cleanup failed: {ex.Message}");
+ }
+ }
+#pragma warning restore SYSLIB0014
+
+ [ConditionalFact(typeof(EnterpriseTestConfiguration), nameof(EnterpriseTestConfiguration.Enabled))]
+#pragma warning disable SYSLIB0014 // WebRequest, FtpWebRequest, and related types are obsolete
+ public void FtpUploadWithSsl_StreamDisposal_NoProtocolViolation()
+ {
+ string fileName = $"test_{Guid.NewGuid()}.txt";
+ string url = $"ftp://{FtpUsername}:{FtpPassword}@{FtpServerUrl}/ftp/{fileName}";
+ byte[] data = Encoding.UTF8.GetBytes("Test data for stream disposal");
+
+ _output.WriteLine($"Testing FTP upload stream disposal with SSL to: {url}");
+
+ // This test specifically validates that stream disposal doesn't cause protocol violations
+ // which was the issue reported in dotnet/runtime#123135
+ FtpWebRequest request = (FtpWebRequest)WebRequest.Create(url);
+ request.Method = WebRequestMethods.Ftp.UploadFile;
+ request.EnableSsl = true;
+ request.Credentials = new NetworkCredential(FtpUsername, FtpPassword);
+
+ // The exception "The underlying connection was closed: The server committed a protocol violation"
+ // used to occur when the request stream was disposed
+ using (var stream = request.GetRequestStream())
+ {
+ stream.Write(data, 0, data.Length);
+ } // Stream disposal should not throw
+
+ using (FtpWebResponse response = (FtpWebResponse)request.GetResponse())
+ {
+ _output.WriteLine($"Response: {response.StatusCode} - {response.StatusDescription}");
+ Assert.Equal(FtpStatusCode.ClosingData, response.StatusCode);
+ }
+
+ // Cleanup
+ try
+ {
+ FtpWebRequest deleteRequest = (FtpWebRequest)WebRequest.Create(url);
+ deleteRequest.Method = WebRequestMethods.Ftp.DeleteFile;
+ deleteRequest.EnableSsl = true;
+ deleteRequest.Credentials = new NetworkCredential(FtpUsername, FtpPassword);
+ using (FtpWebResponse deleteResponse = (FtpWebResponse)deleteRequest.GetResponse())
+ {
+ _output.WriteLine($"Delete response: {deleteResponse.StatusCode}");
+ }
+ }
+ catch (Exception ex)
+ {
+ _output.WriteLine($"Cleanup failed: {ex.Message}");
+ }
+ }
+#pragma warning restore SYSLIB0014
+ }
+}
diff --git a/src/libraries/System.Net.Requests/tests/EnterpriseTests/README.md b/src/libraries/System.Net.Requests/tests/EnterpriseTests/README.md
new file mode 100644
index 00000000000000..2786fce5600d7a
--- /dev/null
+++ b/src/libraries/System.Net.Requests/tests/EnterpriseTests/README.md
@@ -0,0 +1,14 @@
+# Enterprise Scenario Testing
+
+Detailed instructions for running these tests is located here:
+
+src\libraries\Common\tests\System\Net\EnterpriseTests\setup\README.md
+
+## FTP/SSL Tests
+
+These tests validate FTP operations with SSL/TLS (FTPS) against a ProFTPD server configured in the enterprise test environment.
+
+The tests cover:
+- FTP file upload with explicit SSL/TLS
+- FTP file download with explicit SSL/TLS
+- Proper SSL/TLS stream closure to prevent protocol violations
diff --git a/src/libraries/System.Net.Requests/tests/EnterpriseTests/System.Net.Requests.Enterprise.Tests.csproj b/src/libraries/System.Net.Requests/tests/EnterpriseTests/System.Net.Requests.Enterprise.Tests.csproj
new file mode 100644
index 00000000000000..8a4978626c3135
--- /dev/null
+++ b/src/libraries/System.Net.Requests/tests/EnterpriseTests/System.Net.Requests.Enterprise.Tests.csproj
@@ -0,0 +1,14 @@
+
+
+ $(NetCoreAppCurrent)-unix
+ true
+
+
+
+
+
+
+
+