Jump to content
water

Active Directory UDF (II)

Recommended Posts

When you pass an escaped FQDN to _AD_RenameObject,  function _AD_ObjectExists seems to escape the FQDN a second time which leads to an invalid FQDN.
Seems we need to insert the escape function in _AD_RenameObject as well. Can you please test this modified function?

_AD_RenameObjectEX("CN=geb./test,OU=Users,OU=Computers_W7,OU=GEB,OU=DE,DC=sub01,DC=domain,DC=local"), "geb.test")

Func _AD_RenameObjectEX($sObject, $sCN)

    If Not _AD_ObjectExists($sObject) Then Return SetError(1, 0, 0)
    If StringMid($sObject, 3, 1) <> "=" Then $sObject = _AD_SamAccountNameToFQDN($sObject) ; sAMAccountName provided
    Local $oObject = __AD_ObjGet("LDAP://" & $sAD_HostServer & "/" & $sObject)
    Local $oOU = __AD_ObjGet($oObject.Parent) ; Get the object of the OU/CN where the object resides
    $sCN = "CN=" & _AD_FixSpecialChars($sCN) ; escape all special characters
    $sObject = _AD_FixSpecialChars($sObject, 0, "/#") ; escape some special characters <== This line has been added
    $oOU.MoveHere("LDAP://" & $sAD_HostServer & "/" & $sObject, $sCN)
    If @error Then Return SetError(@error, 0, 0)
    Return 1

EndFunc   ;==>_AD_RenameObjectEX

This code is untested as I do not have write access to our AD!!


My UDFs and Tutorials:

Spoiler

UDFs:
Active Directory (2018-12-03 - Version 1.4.11.0) - Download - General Help & Support - Example Scripts - Wiki
OutlookEX (NEW 2019-03-02 - Version 1.3.5.0) - Download - General Help & Support - Example Scripts - Wiki
Outlook Tools (2019-01-22 - Version 0.1.0.0) - Download - General Help & Support
ExcelChart (2017-07-21 - Version 0.4.0.1) - Download - General Help & Support - Example Scripts
PowerPoint (2017-06-06 - Version 0.0.5.0) - Download - General Help & Support
Excel - Example Scripts - Wiki
Word - Wiki
 
Tutorials:

ADO - Wiki

 

Share this post


Link to post
Share on other sites
Posted (edited)

water,

thank you - we are almost there :)

In your _AD_RenameObjectEX() the line $sObject = _AD_FixSpecialChars($sObject, 0, "/#") is executed too late... The lines:

    Local $oObject = __AD_ObjGet("LDAP://" & $sAD_HostServer & "/" & $sObject)
    Local $oOU = __AD_ObjGet($oObject.Parent) ; Get the object of the OU/CN where the object resides

... are already in need of an escaped object variable (in case $sObject = FQDN). That's why I placed the chars escape a bit higher:

Func _AD_RenameObjectExBySupersonic($sObject, $sCN)

    If Not _AD_ObjectExists($sObject) Then Return SetError(1, 0, 0)
    If StringMid($sObject, 3, 1) <> "=" Then $sObject = _AD_SamAccountNameToFQDN($sObject) ; sAMAccountName provided
    If StringMid($sObject, 3, 1) = "=" Then $sObject = _AD_FixSpecialChars($sObject, 0, "/#") ; <<< NEW LINE <<<
    Local $oObject = __AD_ObjGet("LDAP://" & $sAD_HostServer & "/" & $sObject)
    Local $oOU = __AD_ObjGet($oObject.Parent) ; Get the object of the OU/CN where the object resides
    $sCN = "CN=" & _AD_FixSpecialChars($sCN) ; escape all special characters
    $oOU.MoveHere("LDAP://" & $sAD_HostServer & "/" & $sObject, $sCN)
    If @error Then Return SetError(@error, 0, 0)
    Return 1

EndFunc   ;==>_AD_RenameObjectExBySupersonic

Futhermore with If StringMid($sObject, 3, 1) = "=" Then it checks if an escape is really needed because $sObject can be a SamAccountName as well.

What do you think?

Edited by supersonic

Share this post


Link to post
Share on other sites
Posted (edited)

In many funtions one of the very first lines reads:

If Not _AD_ObjectExists($sObject) Then Return SetError(1, 0, 0)

That's good practice, but will lead to the same behaviour/issue _AD_RenameObject() is affected with...

To me it could be more easy to extend _AD_ObjectExists() to handle an already escaped FQDN string 😄

Edited by supersonic

Share this post


Link to post
Share on other sites

I just started to read about the need to escape characters again. I already tried to get my head around this subject some years ago.
It can become quite complex as you can read here: https://www.rlmueller.net/CharactersEscaped.htm

Right now my spare time is very limited. As I only have read access to our AD I can not create objects with special characters. So you (or someone else) would have to do all testing.
As no one else has run into this problem since 2009 (when I started to work on the UDF) I think the best solution would be to simply create a custom function of _AD_RenameObject and remove _AD_ObjectExists.

Maybe I will fix this bug sooner or later but for the time being this is the best I can offer.


My UDFs and Tutorials:

Spoiler

UDFs:
Active Directory (2018-12-03 - Version 1.4.11.0) - Download - General Help & Support - Example Scripts - Wiki
OutlookEX (NEW 2019-03-02 - Version 1.3.5.0) - Download - General Help & Support - Example Scripts - Wiki
Outlook Tools (2019-01-22 - Version 0.1.0.0) - Download - General Help & Support
ExcelChart (2017-07-21 - Version 0.4.0.1) - Download - General Help & Support - Example Scripts
PowerPoint (2017-06-06 - Version 0.0.5.0) - Download - General Help & Support
Excel - Example Scripts - Wiki
Word - Wiki
 
Tutorials:

ADO - Wiki

 

Share this post


Link to post
Share on other sites
Posted (edited)

water,

thank you for your reply -

It was just a matter of time that someone came across this problem.

So: It will be a pleasure to help out by testing modified/new script code. Maybe other forum members will help out, too. :) I think so...

As for me, all the time/work we spend should keep your UDF as it is now - in a consistent/working state. And our efforts should even avoid "good workarounds".

There are several solutions to this issue. One idea could be to rewrite _AD_FixSpecialChars() and _AD_ObjectExists() to handle already escaped strings. All functions benefit from it.

Edited by supersonic

Share this post


Link to post
Share on other sites
Posted (edited)

water,

as stated before I think modifying _AD_FixSpecialChars() [and _ AD_ObjectExists()] to handle (un-)escaped DNs same time more "elegant", I came up with this first approach - and lots of room for improvements 🙂:

; Removed.

With this it is possible to unesacpe and escape within one function call. Furthermore a DN will be splitted first into its parts to ensure that only ...,XX=TextToUnEscape,... (red colored text) is transformed.

I will take a closer look into _AD_ObjectExists() now.

What do you think? Am I heading in the right direction?

Edited by supersonic
A more functional _AD_FixSpecialChars() will be released soon.

Share this post


Link to post
Share on other sites
Posted (edited)

water,

finally I got some working (and hopefully understandable) script code:

#include ".\..\..\AUTOIT\Include\Water\AD_01.04.11.00\AD.au3"


Global $sTmp = "CN=geb. ,test,OU=Users,OU=Computers_W7,OU=GEB,OU=DE,DC=sub01,DC=domain,DC=local"


_AD_Open()
If (Not @error) Then

    Local $iTmp = _AD_RenameObjectEx($sTmp, "geb.test")
    MsgBox(0, "", StringFormat("@error\t\t = %s\r\n@extended\t = %s\r\nresult/return\t = %s", @error, @extended, $iTmp))

    _AD_Close()
EndIf


; #FUNCTION# =========================================================================================
; Function Name:    _AD_RenameObjectEx()
; Description:      Renames an object within the same OU
;                   [https://docs.microsoft.com/de-de/windows/desktop/api/iads/nf-iads-iadscontainer-movehere/].
; Note(s):          None.
; Syntax:           _AD_RenameObjectEx($sObject, $sCN)
; Parameter(s):     $sObject    - The object (user, group, computer [<COMPUTERNAME$>], distinguished name [CN, OU, ...]) to rename.
;                   $sCN        - The new object name.
; Requirement(s):   #Region #HEADER#
; Return Value(s):  On success - Returns 1, @error = 0, @extended = 0.
;                   On failure - Returns 0, @extended = 0/n, @error:
;                   | 0 - No error.
;                   | 1 - '_AD_ObjectExistsEx()' failure, @extended:
;                   | | 0 - No error.
;                   | | n - '@error'.
;                   | 2 - '$oOU.MoveHere()' failure, @extended:
;                   | | 0 - No error.
;                   | | n - '@error'.
; Author(s):        Jonathan Clelland, water, Supersonic!
; Example(s):       None.
; ====================================================================================================
Func _AD_RenameObjectEx( _
        $sObject, _
        $sCN)
    Local $iReturn = 0
    ; !!! ------------------------------------------------------------------------------------------------
    If (StringRegExp($sObject, "(?i)^[CN=|OU=|DC=]{3}") = 1) Then ; DistinguishedName?
        $sObject = _AD_SamAccountNameToFQDN($sObject) ; SamAccountName provided.
        $sObject = _AD_FixSpecialCharsEx($sObject, 1 + 4) ; (Un-)escape all special characters.
    EndIf
    ; ----------------------------------------------------------------------------------------------------
    If (_AD_ObjectExistsEx($sObject) = 1) Then
        Local Const $sLDAP = StringFormat("LDAP://%s/%s", $sAD_HostServer, $sObject)
        Local $oOU = __AD_ObjGet(__AD_ObjGet($sLDAP).Parent()) ; Get the object of the OU/CN where the object resides.
        $oOU.MoveHere($sLDAP, StringFormat("CN=%s", _AD_FixSpecialCharsEx(StringRegExpReplace($sCN, "(?i)^CN=", ""), 1 + 4))) ; (Un-)escape all special characters.
        If (Not @error) Then
            $iReturn = 1
        Else ; Error.
            SetError(2, @error)
        EndIf
        $oOU = 0 ; !!!
    Else ; Error.
        SetError(1, @error)
    EndIf
    Return $iReturn
EndFunc   ;==>_AD_RenameObjectEx


; #FUNCTION# =========================================================================================
; Function Name:    _AD_ObjectExistsEx()
; Description:      Checks if an object exists based on its name/distinguished name and opt. for a given property
;                   [https://www.rlmueller.net/ADOSearchTips.htm].
; Note(s):          None.
; Syntax:           _AD_ObjectExistsEx([$sObject = Default [, $sProperty = Default]])
; Parameter(s):     $sObject    - The object (user, group, computer [<COMPUTERNAME$>], distinguished name [CN, OU, ...]) to check for existence.
;                   $sProperty  - The property to check with. If omitted, depending on the object type the property 'samAccountName' or 'distinguishedName' will be used.
; Requirement(s):   #Region #HEADER#
; Return Value(s):  On success - Returns 1, @error = 0, @extended = 0.
;                   On failure - Returns 0, @extended = 0/n, @error:
;                   | 0 - No error.
;                   | 1 - 'IsObj()' failure.
;                   | 2 - '$iRecordCount' failure (#1 [= 0]).
;                   | 4 - '$iRecordCount' failure (#2 [> 1]), @extended:
;                   | | 0 - No error.
;                   | | n - '$iRecordCount'.
; Author(s):        Jonathan Clelland, water, Supersonic!
; Example(s):       None.
; ====================================================================================================
Func _AD_ObjectExistsEx( _
        $sObject = Default, _
        $sProperty = Default)
    Local $iReturn = 0
    ; !!! ------------------------------------------------------------------------------------------------
    If ((IsKeyword($sObject) > 0) Or (Not StringLen($sObject)) Or ($sObject = -1)) Then $sObject = @UserName
    If ((IsKeyword($sProperty) > 0) Or (Not StringLen($sProperty)) Or ($sProperty = -1)) Then
        $sProperty = "samAccountName"
        If (StringRegExp($sObject, "(?i)^[CN=|OU=|DC=]{3}") = 1) Then ; DistinguishedName?
            $sObject = _AD_FixSpecialCharsEx($sObject, 1 + 4) ; (Un-)escape all special characters.
            $sProperty = "distinguishedName"
        EndIf
    EndIf
    ; ----------------------------------------------------------------------------------------------------
    $__oAD_Command.CommandText() = StringFormat("<LDAP://%s/%s>;(%s=%s);ADSPath;Subtree", $sAD_HostServer, $sAD_DNSDomain, $sProperty, $sObject)
    Local $oRecordSet = $__oAD_Command.Execute() ; Try to retrieve the object property "ADSPath" ...
    If (IsObj($oRecordSet) = 1) Then
        Local Const $iRecordCount = $oRecordSet.RecordCount()
        If ($iRecordCount = 1) Then
            $iReturn = 1
        ElseIf ($iRecordCount > 1) Then ; Error.
            SetError(4, $iRecordCount)
        Else ; Error.
            SetError(2, 0)
        EndIf
        $oRecordSet = 0 ; !!!
    Else ; Error.
        SetError(1, 0)
    EndIf
    Return $iReturn
EndFunc   ;==>_AD_ObjectExistsEx


; #FUNCTION# =========================================================================================
; Function Name:    _AD_FixSpecialCharsEx()
; Description:      (Un-)escapes special characters in a distinguished name or LDAP filter
;                   [https://community.spiceworks.com/topic/444635-powershell-ad-ou-and-splitting-or-substring/],
;                   [https://www.autoitscript.com/forum/topic/112674-regular-expression-to-escape-characters/],
;                   [https://www.autoitscript.com/forum/topic/156987-escape-special-characters/],
;                   [https://www.autoitscript.com/forum/topic/199021-regex-only-at-startend-for-all-chars-of-group/],
;                   [https://www.regex101.com/],
;                   [https://www.rlmueller.net/CharactersEscaped.htm],
;                   [https://www.stackoverflow.com/questions/39794550/how-should-i-escape-commas-in-active-directory-filters/].
; Note(s):          None.
; Syntax:           _AD_FixSpecialCharsEx(Const $sText [, $iOptions = Default [, $sEscapeChars = Default [, $bUnescapeBlanks = Default]]])
; Parameter(s):     $sText              - The object (user, group, computer [<COMPUTERNAME$>], distinguished name [CN, OU, ...]) to rename.
;                   $iOptions           - The instruction on how to (un-)escape - add the options together for multiple operations:
;                   | 0 - Do nothing.
;                   | 1 - Unescape.
;                   | 2 - Escape (default/standard).
;                   | 4 - Escape (LDAP filter).
;                   $sEscapeChars       - The character set to use for (un-)escaping.
;                   $bUnescapeBlanks    - The instruction blanks also being unescaped:
;                   | False - Do not unescape blanks (default/fallback/standard).
;                   | True  - Unescape blanks.
; Requirement(s):   #Region #HEADER#
; Return Value(s):  On success - Returns (un-)escaped string, @error = 0, @extended = 0.
;                   On failure - Returns unchanged input string, @extended = 0/n, @error:
;                   | 0 - No error.
;                   | 1 - 'StringLen()' failure.
;                   | 2 - 'StringRegExp()' failure, @extended:
;                   | | 0 - No error.
;                   | | n - '@error'.
;                   | 4 - '_ArrayToString()' failure, @extended:
;                   | | 0 - No error.
;                   | | n - '@error'.
; Author(s):        Jonathan Clelland, water, Supersonic!
; Example(s):       None.
; ====================================================================================================
Func _AD_FixSpecialCharsEx( _
        Const $sText, _
        $iOptions = Default, _
        $sEscapeChars = Default, _
        $bUnescapeBlanks = Default)
    Local $sReturn = $sText ; Fallback (input = output).
    ; !!! ------------------------------------------------------------------------------------------------
    If ((IsKeyword($iOptions) > 0) Or (Not StringLen($iOptions)) Or ($iOptions = -1)) Then $iOptions = 2
    If ((IsKeyword($sEscapeChars) > 0) Or (Not StringLen($sEscapeChars)) Or ($sEscapeChars = -1)) Then $sEscapeChars = '"\/#,+<>;='
    If ((IsKeyword($bUnescapeBlanks) > 0) Or (Not StringLen($bUnescapeBlanks)) Or ($bUnescapeBlanks == -1)) Then $bUnescapeBlanks = False
    ; ----------------------------------------------------------------------------------------------------
    If (StringLen($sText) > 0) Then
        Local Const $bDN = (StringRegExp($sText, "(?i)^[CN=|OU=|DC=]{3}") = 1) ; DistinguishedName?
        Local $aText[1] = [$sText]
        Switch $bDN
            Case True
                $aText = StringRegExp($sText, "(?i)(?:CN|OU|DC)=.*?(?=(?<!\\),OU|,DC)|DC=.*$", 3) ; Assertion: negative look-behind.
                If (Not @error) Then
                    ContinueCase ; !!!
                Else ; Error.
                    SetError(2, @error)
                EndIf
            Case Else ; Case False
                Switch BitAND($iOptions, 1)
                    Case 1  ; Unescape.
                        Local $sEscapeCharsRegExp = StringStripWS($sEscapeChars, 8) ; $STR_STRIPALL
                        Switch $bUnescapeBlanks
                            Case True
                                $sEscapeCharsRegExp = StringFormat(" %s", $sEscapeCharsRegExp)
                                ContinueCase ; !!!
                            Case Else ; Case False
                                Local Const $aEscapeCharsRegExp = StringSplit($sEscapeCharsRegExp, "", 2)
                                For $i = 0 To UBound($aText) - 1 Step 1
                                    For $j In $aEscapeCharsRegExp
                                        If (Not StringRegExp($aText[$i], StringRegExpReplace($j, "(.)", "(\\\\\\$1)|(\\\\5C\\$1)"))) Then ContinueLoop (1) ; !!!
                                        $aText[$i] = StringRegExpReplace(StringRegExpReplace($aText[$i], StringFormat("\\\\5C(%s)", $j), "$1"), StringFormat("\\\\(%s)", $j), "$1") ; All occurences: '$j = ".{1}"'.
                                    Next
                                Next
                        EndSwitch
                        ContinueCase ; !!!
                    Case Else
                        Local $bAlreadyEscaped = False
                        Local $sPrefix = "" ; DistinguishedName.
                        Switch BitAND($iOptions, 2)
                            Case 2 ; Escape (default/standard).
                                For $i = 0 To UBound($aText) - 1 Step 1
                                    $sPrefix = ((Not $bDN) ? ("") : (StringLeft($aText[$i], 3)))
                                    $aText[$i] = $sPrefix & StringRegExpReplace(((Not $bDN) ? ($aText[$i]) : (StringMid($aText[$i], 4))), "(?<!\\)([" & $sEscapeChars & "])", "\\$1") ; Assertion: negative look-behind.
                                Next
                                $bAlreadyEscaped = (Not $bAlreadyEscaped) ; Toggle.
                                ContinueCase ; !!!
                            Case Else
                                Switch BitAND($iOptions, 4)
                                    Case 4 ; Escape (LDAP filter).
                                        If (Not $bAlreadyEscaped) Then
                                            For $i = 0 To UBound($aText) - 1 Step 1
                                                $sPrefix = ((Not $bDN) ? ("") : (StringLeft($aText[$i], 3)))
                                                $aText[$i] = $sPrefix & StringRegExpReplace(Execute('"' & StringRegExpReplace(((Not $bDN) ? ($aText[$i]) : (StringMid($aText[$i], 4))), "(\*|\(|\)|\\(?![[:xdigit:]]{2}))", '" & "\\" & Hex(AscW("$1"), 2) & "') & '"'), "(NUL)", "\\00")
                                                ; Several special characters must be escaped with either "\" or "\5C" when used in a LDAP search filter:
                                                $aText[$i] = $sPrefix & StringRegExpReplace(((Not $bDN) ? ($aText[$i]) : (StringMid($aText[$i], 4))), '(\,|\\|\#|\+|\<|\>|\;|\"|\=(?![[:xdigit:]]{2}))', "\\$1")
                                                $aText[$i] = $sPrefix & Execute("'" & StringRegExpReplace(((Not $bDN) ? ($aText[$i]) : (StringMid($aText[$i], 4))), "(?|^(\h*)|(\h*)$)", "' & StringReplace('$1', ' ', '\\ ') & '") & "'")
                                            Next
                                        EndIf
                                        ContinueCase ; !!!
                                    Case Else
                                        Local Const $sResult = _ArrayToString($aText, ",")
                                        If (Not @error) Then
                                            $sReturn = $sResult
                                        Else ; Error.
                                            SetError(4, @error)
                                        EndIf
                                EndSwitch
                        EndSwitch
                EndSwitch
        EndSwitch
    Else ; Error.
        SetError(1, 0)
    EndIf
    Return $sReturn
EndFunc   ;==>_AD_FixSpecialCharsEx

And, yes, it is indeed a bit overcomplicated and somehow blown up :)

Now it is possible (at least for me but I think for others, too) to rename objects with whatever formatted names and in either direction. It doesn't matter if it is already escaped or needs to be unescaped first.

The current _AD_FixSpecialChars() is incomplete regarding $iOption = 3. Please read [https://stackoverflow.com/questions/39794550/how-should-i-escape-commas-in-active-directory-filters/].

Edited by supersonic

Share this post


Link to post
Share on other sites

I guess you have already noticed that I started a poll to get an idea how many people use special characters in Distinguished Names.
Till now only 4 members have participated and no one uses special characters.

So my conclusio for the moment is:
If you are happy with the solution you have created for yourself, I would just move the subject to the to-do list. If there is pressing demand by multiple users we could start working on the subject again.
What do you think?


My UDFs and Tutorials:

Spoiler

UDFs:
Active Directory (2018-12-03 - Version 1.4.11.0) - Download - General Help & Support - Example Scripts - Wiki
OutlookEX (NEW 2019-03-02 - Version 1.3.5.0) - Download - General Help & Support - Example Scripts - Wiki
Outlook Tools (2019-01-22 - Version 0.1.0.0) - Download - General Help & Support
ExcelChart (2017-07-21 - Version 0.4.0.1) - Download - General Help & Support - Example Scripts
PowerPoint (2017-06-06 - Version 0.0.5.0) - Download - General Help & Support
Excel - Example Scripts - Wiki
Word - Wiki
 
Tutorials:

ADO - Wiki

 

Share this post


Link to post
Share on other sites
Posted (edited)

Haven't seen it until now... Now 5 members are participating. 🙂

I think your great UDF deserves this enhancement/improvement! And I think this is a bug because using special chars in DNs is valid and at least some functions can't handle such DNs or handling is incomplete. Using special chars in DNs is of course not sooo good or even bad practice. But sometimes it can't be avoided or it is out of your responsibility... :(

And, yes, I could live with my "solution"... In the "near future" I have to tweak nearly all related functions to impove DN handling. My thought is to do it _once_ (togehter here in the forum) and and let others benefit, too.

What do you mean with "to-do list"? I often made the experience that such lists meaning "never will be done".

I can offer to do part of the testing - will be a pleasure to me!

Now it is up to you. 😉

Edited by supersonic

Share this post


Link to post
Share on other sites
Posted (edited)

I reworked _AD_FixSpecialCharsEx() to make unescaping more reliable. Please see functions posted above.

Edited by supersonic
... and reworked _AD_RenameObjectEx() also.

Share this post


Link to post
Share on other sites

As you might have noticed only 4 users answered the poll. Only a single user uses special characters in Distinguished Names (this one is you).
In regard of the low demand, my limited spare time and limited access to an AD to test I think i will not go this route.

Maybe the other way round is sensible for you: remove the special characters from your objects :huh:
This way you could stick with the unmodified AD UDF :)


My UDFs and Tutorials:

Spoiler

UDFs:
Active Directory (2018-12-03 - Version 1.4.11.0) - Download - General Help & Support - Example Scripts - Wiki
OutlookEX (NEW 2019-03-02 - Version 1.3.5.0) - Download - General Help & Support - Example Scripts - Wiki
Outlook Tools (2019-01-22 - Version 0.1.0.0) - Download - General Help & Support
ExcelChart (2017-07-21 - Version 0.4.0.1) - Download - General Help & Support - Example Scripts
PowerPoint (2017-06-06 - Version 0.0.5.0) - Download - General Help & Support
Excel - Example Scripts - Wiki
Word - Wiki
 
Tutorials:

ADO - Wiki

 

Share this post


Link to post
Share on other sites

water,

thank you for your (final) statement regarding this. Your are right if time and interest matters... If you ever like to address this issue at some point in the future, please let me now. I’m still interested to help improving your UDF.

BTW: Our objects are 99,99% „special-characters-free“. 🙂 But Enterprise-wide there are several ADs I don’t have write access to - respectively I am not responsible for them. As long I got a reliable/working solution for such „unavoidable circumstances“ I’m happy.

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×
×
  • Create New...