Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Adding Linux support to System.DirectoryServices.Protocols #35380

Merged
merged 10 commits into from May 7, 2020

Conversation

joperezr
Copy link
Member

Fixes #23944

Creating this as a draft PR first as I expect a lot of feedback plus some things will still need to happen in order to get tests working in Linux, but I want to start getting feedback so I can address it while I finish setting up running tests in Linux. This adds support to Linux for the System.DirectoryServices.Protocols namespace, by simply creating a PAL layer that abstracts the calls between wldap32 and openldap. Given both native libraries are just implementing ldap protocol, most of their APIs are the same with very similar or same structures which makes it so that managed code didn't actually have to change that much, we just change the native calls we do underneath. There are some open questions, like for example, there are some settings that are specific to wldap32 and not supported on any other library, the question is, should they be No-Ops for compatibility with previous apps working in Windows or should they instead throw PNSE?

One thing that is missing from this PR is the support for Default Credentials. That means that if you have your code running on a Kestrel Server which is domain-joined to an AD and have a valid session token already authenticated in the machine, then you wouldn't need to pass in credentials again when creating an LDAPConnection since you would just reuse that token. That works today in Windows, and I'm working on fixing the native calls on Bind to make sure that it also works on Linux. That support will come either as part of this PR later, or it will be added as a subsequent PR.

cc: @ericstj @tarekgh @bartonjs @stephentoub @davidsh

FYI: @JunTaoLuo @Tratcher

@tarekgh
Copy link
Member

tarekgh commented Apr 23, 2020

CC @tquerec

public const string LDAP_SASL_AUTOMATIC = "0";
}

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
Copy link
Member

Choose a reason for hiding this comment

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

CharSet = CharSet.Unicode [](start = 41, length = 25)

why this needed here?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm actually not sure, this was the way this struct already existed, I just moved them to a new file.

SetHandle(ldap);
}

internal ConnectionHandle(IntPtr value, bool disposeHandle) : base(true)
Copy link
Member

Choose a reason for hiding this comment

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

disposeHandle here seems to be redundant with the one from the base class... which you're always giving the literal true.

If it means "false to never call ReleaseHandle", that's exactly what the base bool ownsHandle is for.

fPerformRelease = ((oldState & (SH_State_RefCount | SH_State_Closed)) == SH_RefCountOne) && m_ownsHandle;
if (fPerformRelease)
{
GCPROTECT_BEGIN(sh);
CLR_BOOL fIsInvalid = FALSE;
DECLARE_ARGHOLDER_ARRAY(args, 1);
args[ARGNUM_0] = OBJECTREF_TO_ARGHOLDER(sh);
PREPARE_SIMPLE_VIRTUAL_CALLSITE_USING_SLOT(s_IsInvalidHandleMethodSlot, sh);
CRITICAL_CALLSITE;
CALL_MANAGED_METHOD(fIsInvalid, CLR_BOOL, args);
if (fIsInvalid)
{
fPerformRelease = false;
}
GCPROTECT_END();
}

Copy link
Member Author

Choose a reason for hiding this comment

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

So is your suggestion to basically instead of having a field for _needDispose and checking this in ReleaseHandle, I should just call base passing in disposeHandle value and not need to check in ReleaseHandle as it would not get called in the case it was set to false right? Just making sure I understood this comment..

Copy link
Member Author

Choose a reason for hiding this comment

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

Actually we do have one problem with doing this. The one in the base I believe can't be overwritten after creation, and we have some (internal) methods in LdapConnection that can flip the _needsDispose, so I guess the reason why the original design was like this, was so that ReleaseHandle would get called always, and we would only actually dispose if we need to.

SetHandle(Wldap32.ldap_init(null, 389));

if (handle == IntPtr.Zero)
{
Copy link
Member

Choose a reason for hiding this comment

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

I feel like the marshaller bypasses ctors when it creates SafeHandles as a return value, (maybe I'm wrong and it just finds a ctor that looks like one it knows how to call? or maybe a default is required and I'm having a very bad recollection day). I'm not sure this code actually prevents invalid handles.

A more canonical representation of this would be to declare the ldap_init call to return a SafeConnectionHandle (instead of IntPtr) and the caller should do

SafeConnectionHandle handle = LdapPal.Initialize(null, 389);

// In LdapPal.Windows.cs

internal static SafeConnectionHandle Initialize(string host, int port)
{
    SafeConnectionHandle handle = Interop.wldap32.ldap_init(host, port);

    if (handle.IsInvalid)
    {
        handle.Dispose();
        // this error handling code
    }

    return handle;
}

Copy link
Member

Choose a reason for hiding this comment

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

I feel like the marshaller bypasses ctors when it creates SafeHandles as a return value

Nope :)

e.g. try this:

using System;
using System.Runtime.InteropServices;

class Program
{
    static void Main() => GetStdHandle(-10);

    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern MySafeHandle GetStdHandle(int nStdHandle);
}

class MySafeHandle : SafeHandle
{
    public MySafeHandle(string hello, string world) : base(IntPtr.Zero, ownsHandle: false) { }
    public override bool IsInvalid => true;
    protected override bool ReleaseHandle() => true;
}

You'll get an exception like this:

Unhandled Exception: System.MissingMethodException: .ctor
   at Program.GetStdHandle(Int32 nStdHandle)
   at Program.Main()


namespace System.DirectoryServices.Protocols
{
internal sealed class ConnectionHandle : SafeHandleZeroOrMinusOneIsInvalid
Copy link
Member

Choose a reason for hiding this comment

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

Since this is used by the P/Invoke declarations in Interop, I'd put this file in that same directory (similar to the Windows one). e.g. https://github.com/dotnet/runtime/blob/master/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.OCSP.cs#L103-L134

Copy link
Member

Choose a reason for hiding this comment

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

(Following the OCSP example, you could just nestle the appropriate SafeHandle types in the bottom of the Interop.Ber.cs and Interop.Ldap.cs files, so that if some other library needs them it's just one include)

public static IEnumerable<object[]> Ctor_BeforeCount_AfterCount_ByteArrayTarget_Data()
{
yield return new object[] { 0, 0, null, (PlatformDetection.IsWindows) ? new byte[] { 48, 132, 0, 0, 0, 18, 2, 1, 0, 2, 1, 0, 160, 132, 0, 0, 0, 6, 2, 1, 0, 2, 1, 0 } : new byte[] { 48, 14, 2, 1, 0, 2, 1, 0, 160, 6, 2, 1, 0, 2, 1, 0 } };
yield return new object[] { 10, 10, new byte[] { 1, 2, 3 }, (PlatformDetection.IsWindows) ? new byte[] { 48, 132, 0, 0, 0, 11, 2, 1, 10, 2, 1, 10, 129, 3, 1, 2, 3 } : new byte[] { 48, 11, 2, 1, 10, 2, 1, 10, 129, 3, 1, 2, 3 } };
Copy link
Member

Choose a reason for hiding this comment

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

For long-term test-building maintenance, consider a helper. E.g.

yield return new object[] { 0, 0, null, FormatLengths("30{0}020100020100A0{1}020100020100", 18, 6) };

...

private static byte[] FormatLengths(string hexFormat, params int[] lengths)
{
    string[] hexLengths = new string[lengths.Length];

    for (int i = 0; i < lengths.Length; i++)
    {
        int curLen = lengths[i];

        if (PlatformDetection.IsWindows)
        {
            hexLengths[i] = $"84{curLen:X8}";
        }
        else if (curLen < 0x80)
        {
            hexLengths[i] = curLen.ToString("X2");
        }
        else if (curLen <= byte.MaxValue)
        {
            hexLengths[i] = $"81{curLen:X2}";
        }
        else if (curLen <= ushort.MaxValue)
        {
            hexLengths[i] = $"82{curLen:X4}";
        }
        else if (curLen <= 0xFFFFFF)
        {
            hexLengths[i] = $"83{curLen:X6}";
        }
        else
        {
            hexLengths[i] = $"84{curLen:X8}";
        }
    }

    string hex = string.Format(hexFormat, hexLengths);
    // This presupposes something like the crypto tests have:
    return hex.HexToByteArray();
}

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree this would be valuable, but if that's ok with you I would rather do that in a subsequent PR so to not add more complexity to this PR for now. Once I have tests running on Mono and OSX and passing, then I think it would be a great time to address this.

Copy link
Member

Choose a reason for hiding this comment

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

Deferring the test helper is totally fine.

@joperezr joperezr merged commit 55d3260 into dotnet:master May 7, 2020
@joperezr joperezr deleted the SDSP branch May 7, 2020 20:37
@joperezr
Copy link
Member Author

joperezr commented May 7, 2020

Thanks a lot to all for the reviews! 😄

@danmoseley
Copy link
Member

Nice! 🔥

@JunTaoLuo
Copy link
Contributor

🎆

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

Successfully merging this pull request may close these issues.

Support System.DirectoryServices.Protocols on Linux/Mac
9 participants