lørdag den 26. marts 2011

PowerShell PSCmdLets and Pipeline objects

I’m working on a PowerShell interface to the project I’m working on. Most of the commands return Class’s and I'm being very thorough about making sure passing parameters work both normally, unnamed and piped. But one function kept giving me problems.

PS C:\> $customerservice = Get-CustomerService skadefro.dk
PS C:\
> $customerservice | Get-CustomerServiceObject
Get
-CustomerServiceObject : Need to suply either CustomerServiceID or CustomerServiceObjectID
At line:
1 char:45
+ $customerservice | Get-CustomerServiceObject <<<<
+ CategoryInfo : NotSpecified: (:) [Get-CustomerServiceObject], E xception
+ FullyQualifiedErrorId : System.Exception,Cloud.Provisioning.CloudSnapin.
Cloud.Provisioning.GetCustomerServiceObject

PS C:\
> Get-CustomerServiceObject $customerservice

ExtensionData : System.Runtime.Serialization.ExtensionDataObject
CustomerServiceID :
1897
CustomerServiceObjectID :
2260
Label : 36cb002b
-355e-4dd3-9dca-f217922a4a63
Properties : {
2260, 2260, 2260, 2260...}
ServiceObjectID :
15
default : False
private :
True

ExtensionData : System.Runtime.Serialization.ExtensionDataObject
CustomerServiceID :
1897
CustomerServiceObjectID :
2261
Label : c38d1601
-df38-40d8-8ef8-6f8ac96eb09e
Properties : {
2261, 2261, 2261, 2261...}
ServiceObjectID :
15
default : False
private :
True

PS C:\
>

I can pass it as a normal parameter, but when pipelining the object(s) it wouldn’t get the object. I used the same parameter type in other CMDlets with no problems. I even tried deleting the project and then class by class copy’n’pasting over the code in case something behind the scene was messing with me, but to no wail. Then as I for the 1000’th time was giving it a crack, I fell a cross a news post (sorry I cant find the link again ) that instantly made it clear to me what was wrong.


 


PS C:\> trace-command parameterbinding -pshost { $customerservice | Get-Customer
ServiceObject }
This is one cool command. You can do all kinds of neat tricks with that, but most importantly right now, it showed me how PowerShell tries to pass the piped objects, and turns out my CMD let was throwing an exception every time PowerShell tried giving it the parameter
Protected Overrides Sub BeginProcessing()
If _CustomerServiceObjectID.Count = 0 And _CustomerServiceID.Count = 0 Then
Throw New Exception("Need to suply either CustomerServiceID or CustomerServiceObjectID")
End If
End Sub

Once I removed my check from BeginProcessing and moved it to ProcessRecord everything worked as intended.

tirsdag den 22. marts 2011

Calling WCF Web Service from DLL

I need to call a webservice from a DLL ( powershell PSCmdlet ) but when testing it I keept getting below error

PS C:\Users\administrator.INT> Get-CustomerServiceObject -CustomerServiceID 1880
Get-CustomerServiceObject : Could not find default endpoint element that references contract 'webserviceCustomerService
s.CustomerServices' in the ServiceModel client configuration section. This might be because no configuration file was f
ound for your application, or because no endpoint element matching this contract could be found in the client element.
At line:1 char:26
+ Get-CustomerServiceObject <<<<  -CustomerServiceID 1880
    + CategoryInfo          : NotSpecified: (:) [Get-CustomerServiceObject], InvalidOperationException
    + FullyQualifiedErrorId : System.InvalidOperationException,Cloud.Provisioning.CloudSnapin.Cloud.Provisioning.GetCu
   stomerServiceObject

PS C:\Users\administrator.INT>

One way to fix this would be to add the system.serviceModel from app.config to powershell’s .config file,but that just sound wrong in my ears. Google !

So found this post and re-wrote it to my own VB.NET version

'Dim wsCustomerServices As New webserviceCustomerServices.CustomerServicesClient
Dim wsCustomerServices As webserviceCustomerServices.CustomerServices
Try
    Dim asm As System.Reflection.Assembly = System.Reflection.Assembly.GetExecutingAssembly
    Dim assemName As System.Reflection.AssemblyName = asm.GetName()
    Dim dllPath As String = asm.CodeBase.Replace("file:///", "")
    dllPath = IO.Path.GetDirectoryName(dllPath)

    Dim stockConfiguration As Configuration = ConfigurationManager.OpenMappedExeConfiguration(New ExeConfigurationFileMap() With {.ExeConfigFilename = (dllPath & "\Cloud.Provisioning.CloudSnapin.dll.config")}, ConfigurationUserLevel.None)
    Dim stockChannelFactory As ConfigurationChannelFactory(Of webserviceCustomerServices.CustomerServices)
    stockChannelFactory = New ConfigurationChannelFactory(Of webserviceCustomerServices.CustomerServices)("CustomBinding_CustomerServices", stockConfiguration, Nothing)
    wsCustomerServices = stockChannelFactory.CreateChannel()

Catch ex As Exception
    Throw ex
End Try

mandag den 21. marts 2011

AXFR Zone Transfers from PowerShell

So I’m playing around with a script to transfer a zone from another DNS server to my own DNS servers. I’m using DnsShell for most of my operation’s ( it has a few bugs, but nothing that cant be handled by adding abit of WMI  ) but I couldn’t get AXFR requests to work. google sendt me to PowerShell Dig PoshNet but that didn’t work either, and then it hit me (I can be so slow sometimes), I can’t AXFR from my default DNS server I need to ask hosting DNS server, and no need to bother with different PSSnapins. DnsShell does the work just fine. Here is an example.

$zonename = ‘somedomain.com’
$nameservers = (Get-Dns $zonename ns) | Select-Object -ExpandProperty Answer
$dnsserver = (get-dns $nameservers[0].RecordData).Answer[0].RecordData
Write-Host (‘Asking ‘ + $nameservers[0].RecordData + ' ' + $dnsserver)
$zone = Get-Dns -Name $zonename -RecordType axfr -Server $dnsserver | Select-Object -ExpandProperty Answer
$zone

WinRM Remote Management and PowerShell certificates

So I was playing around with DNS. I am configurering that remote though PowerShell. After I had most things working tested against NS1 I wanted to start working on the secondary Zone’s on NS2 and NS3 and started getting

$PSSession = New-PSSession -auth CredSSP -cred $cred -ComputerName ns2 -UseSSL;

[ns2] Connecting to remote server failed with the following error message : The WinRM client received an HTTP server error status (500), but the remote service did not include any other information about the cause of the failure. For more information, see the about_Remote_Troubleshooting Help topic.
    + CategoryInfo          : OpenError: (System.Manageme....RemoteRunspace:RemoteRunspace) [], PSRemotingTransportException
    + FullyQualifiedErrorId : PSSessionOpenFailed

That’s weird, I know it was configured. Ive been playing around with certificates while configurering SCOM 2007 so I knew all the certificates might not be there, so I decided to reconfigure it all. Looking in the event log on the remote machine NS2 I also found

The WinRM service failed to initialize CredSSP.

Additional Data
The error received was 0x80338082.

User Action
Configure CertificateThumbprint setting under the WinRM configuration for the service. Use the thumbprint of a valid certificate and make sure that Network Service has access to the private key of the certificate.

Searching google for that error kept mentioning making sure to use the right certificate. Sure enough, the listener was using a certificate that wasn’t there. ( run “winrm enumerate winrm/config/listener” and check the CertificateThumbprint against the certificates in the local machine MY store )

so I decided to just run my initial powershell script

winrm delete winrm/config/listener?Address=*+Transport=HTTP
winrm delete winrm/config/listener?Address=*+Transport=HTTPS
winrm quickconfig -transport:http
winrm quickconfig -transport:https
Enable-WSManCredSSP -Role server

But no use, I was still getting the above error. Then I looked at the permissions of the certificates and sure enough “network services” didn’t have permissions. ( MMC –> CTRL+M –> Certificates –> Computer account ) ( Personal –> Certificates –> Right click certificate –> All tasks –> Manage Private Keys ..”  ) By the way several sites recommend using “Find Private Key tool” from WCF SDK. No need to do that, powershell is your friend here.

I was still getting the error. then it hit me … The Service also have a CertificateThumbprint ( run “winrm get winrm/config” )

So I wrapped it all up in a little script.

# Get local computer name and FQDN
$computername = get-content  env:computername
$objIPProperties = [System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties()
$fqdn = "{0}.{1}" -f $objIPProperties.HostName, $objIPProperties.DomainName
$fqdn = $fqdn.ToLower()

# works like a charm if only one certificate is in the the store.
winrm quickconfig -quiet -transport:http
winrm quickconfig -quiet -transport:https

# allow basic authentication
winrm p winrm/config/service '@{AllowUnencrypted="true"}'
winrm p winrm/config/service/auth '@{Basic="true"}'

# Get all local certificates with computer name/fqdn and grant network service permissions on it
$certs = dir cert:\LocalMachine\my
ForEach($cert in $certs){
    if( ($cert.Subject -eq ('CN=' + $computername)) -or ($cert.Subject -eq ('CN=' + $fqdn))){
        Write-Host ("fix permissions on " + $cert.Thumbprint + " " + $cert.Subject)
        $location = $cert.PrivateKey.CspKeyContainerInfo.UniqueKeyContainerName
        $folderlocation = gc env:ALLUSERSPROFILE
        $folderlocation = $folderlocation + "\Microsoft\Crypto\RSA\MachineKeys\"
        $filelocation = $folderlocation + $location
        #icacls $filelocation /grant "Network service:(OI)(CI)(F)"
        icacls $filelocation /grant "Network service:(F)"
    }
}

# Force listener and service to use last known good certificate
winrm set winrm/config/Listener?Address=*+Transport=HTTPS ('@{CertificateThumbprint="' + $cert.Thumbprint + '"}')
winrm p winrm/config/service ('@{CertificateThumbprint="' + $cert.Thumbprint + '"}')

# if first time run, set the role to server
Enable-WSManCredSSP -Role server

lørdag den 19. marts 2011

The SSL certificate is signed by an unknown certificate authority

So while I was playing with certificates installation, I was also playing around with my PowerShell DLL and I suddenly started getting this error

[PS] C:\>$cred = New-Object System.Management.Automation.PSCredential $Username, $Password
[PS] C:\>$PSSession = New-PSSession -auth CredSSP -cred $cred -ComputerName ns1 -UseSSL
$PSSession = New-PSSession -auth CredSSP -cred $cred -ComputerName ns1 –UseSSL

$PSSession = New-PSSession -auth CredSSP -cred $cred -ComputerName ns1 –UseSSL

[ns1] Connecting to remote server failed with the following error message : The server certificate on the destination computer (ns1:5986) has the following errors:
The SSL certificate could not be checked for revocation. The server used to check for revocation might be unreachable.
The SSL certificate is signed by an unknown certificate authority. For more information, see the about_Remote_Troubleshooting Help topic.
    + CategoryInfo          : OpenError: (System.Manageme....RemoteRunspace:Re   moteRunspace) [], PSRemotingTransportException
    + FullyQualifiedErrorId : PSSessionOpenFailed

at first I couldn’t quite understand why. My machine was part of the domain so I started checking if the machines could see/connect to the CA and such, but then I remembered one of the functions I was testing while playing around with certificates was importing the root CA to trusted root certification authorities, and when I checked, it was truly missing. I came across this Blog post that makes a good explanation that others might find useful in case they are using self signed certificates instead of certificates from a CA like me.

onsdag den 16. marts 2011

RTFM , learned the hard way

So I was fixing a few bug’s in an application that supports auto updating. Everything was perfect until I suddenly noticed that non of the machines with the application was updating anymore. so I log on to one of the machines and see the service isn't running on the machine either (the application is a windows service). Weird, no errors in the evenlog from the service either, but all my services can also be run from command so I give that a go.

Unhandled Exception: System.BadImageFormatException: Could not load file or assembly XXXXX.exe' or one of its dependencies. This assembly is built by a runtime newer than the currently loaded runtime and cannot be loaded.
File name: 'XXXXX.exe'

WTF ? hmm, looks like my app.config is screwed so I create a new one and try again.

image
To run this application, you first must install one of the following versions of the .Net Framework: v4.0.30319

that’s weird, I was 100% sure my application was set to Target Framework v3.5. I triede every single combination from 2.0, 3.0, 3.5, 3.5 client profile to 4.0. No matter what I did, it only worked when set to 4.0 AND .net 4.0 was installed on the client. But I don’t want 4.0 on all machines.

After ton of time on google, I realised something. When changing target framework the path to the dll’s in the project references keept pointing to C:\Windows\Microsoft.NET\Framework\v4.0.30319 or C:\Windows\Microsoft.NET\Framework64\v4.0.30319 depending on my target CPU type.

Even if I manuly removed all references and added them pointing to C:\Windows\Microsoft.NET\Framework\v2.0.50727 or C:\Windows\Microsoft.NET\Framework64\v2.0.50727 visual studio 2010 would accept it, but chance the path back to the v4.0.30319 directory.

THIS WAS DRIVING ME NUTS! Sad smile

Then .. and gods know how I found it .. came a cross this page

and BAM it hit me, I had the exact same issue. I too had googled the problem with

Add-PSSnapin : Cannot load Windows PowerShell snap-in XXXXX because of the following error: Coul not load file or assembly 'file:///C:\XXXXX.dll' or one of its dependencies. This assembly i built by a runtime newer than the currently loaded runtime and cannot be loaded.
At line:1 char:13
+ Add-PSSnapin <<<<  XXXXX
    + CategoryInfo          : InvalidArgument: (XXXXX:String) [Add-PSSnapin], PSSnapInException
    + FullyQualifiedErrorId : AddPSSnapInRead,Microsoft.PowerShell.Commands.AddPSSnapinCommand


and the all around solution you find to this problem is adding 1 or both of these 2 registry keys

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework:OnlyUseLatestCLR
HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\ Microsoft\.NET Framework:OnlyUseLatestCLR

Except, everywhere you find it they also warn about doing it, but noone seemed to have a solution. but there is on and its simple. if you just want to fix it for you own local powershell, just go to $pshome and create a file named powershell.exe.config and add this text to it

<configuration>
<startup useLegacyV2RuntimeActivationPolicy="true">
<supportedRuntime version="v4.0.30319"/>
<supportedRuntime version="v2.0.50727"/>
</startup>
</configuration>

if you want to chance it for all users and all versions (32 bit and 64bit) create the above file in C:\windows\System32\WindowsPowerShell\v1.0 and C:\Windows\SysWOW64\WindowsPowerShell\v1.0

onsdag den 9. marts 2011

Microsoft implements the world’s most stupid error

So I'm working a bit more on my PowerShell class, trying to do some work on remote machines. I add the option to supply a host, username and password and want’s to connect using WSManConnectionInfo. I end up with a ton of weird errors but can’t really figure out what is making all those errors, so desperate as I am, I end up using New-PSSession and Enter-PSSession. ( I should have just sticked with following the error)

I still get errors but now from the Runspace saying its not supported. Digging around on Google I find out I need to implement IHostSupportsInteractiveSession to my PSHost. Well that wasn’t to hard, I add

Imports System.Management.Automation.Host
Public Class CloudPowerShellHost
    Inherits PSHost
    Implements IHostSupportsInteractiveSession

and smack in

Public ReadOnly Property IsRunspacePushed() As Boolean
    Get
        Return Me.pushedRunspace IsNot Nothing
    End Get
End Property

Private pushedRunspace As System.Management.Automation.Runspaces.Runspace = Nothing
Private myRunSpace As System.Management.Automation.Runspaces.Runspace = Nothing

Public Sub PopRunspace() Implements System.Management.Automation.Host.IHostSupportsInteractiveSession.PopRunspace
    myRunSpace = Me.pushedRunspace
    Me.pushedRunspace = Nothing
End Sub

Public Sub PushRunspace(ByVal runspace__1 As System.Management.Automation.Runspaces.Runspace) Implements System.Management.Automation.Host.IHostSupportsInteractiveSession.PushRunspace
    Me.pushedRunspace = Runspace
    myRunSpace = runspace__1
End Sub

Public ReadOnly Property Runspace As System.Management.Automation.Runspaces.Runspace Implements System.Management.Automation.Host.IHostSupportsInteractiveSession.Runspace
    Get
        Return Me.myRunSpace
    End Get
End Property

Public ReadOnly Property IsRunspacePushed1 As Boolean Implements System.Management.Automation.Host.IHostSupportsInteractiveSession.IsRunspacePushed
    Get
        Return Me.pushedRunspace IsNot Nothing
    End Get
End Property

Public Sub SetRunspace(ByVal runspace As System.Management.Automation.Runspaces.Runspace)
    Me.myRunSpace = runspace
End Sub

At the end of the class and I’m good to go. Still after this I’m still getting tons of errors, so I start walking back the stack seeing what I missed. Turns out I forgot some error handling in a function I use, that saved all variables in a runspace, and it is my enumeration of variables that fails.

System.InvalidCastException: Unable to cast object of type 'System.Management.Automation.PSCustomObject' to type 'System.Management.Automation.PSVariable'.

I’m doing something like this

Dim o As System.Management.Automation.PSVariable
Try
    o = ps.BaseObject
Catch ex As Exception
    Throw ex
End Try

I loop all the PSObjects from doing a get-variable so I inspect the PS object and; WTF???

image

image

The BaseObject is “kind of empty” not really sure what it is, but the main object is a variable, so no sweat, I smack in

Dim o As System.Management.Automation.PSVariable
Try
    o = ps.BaseObject
Catch ex As Exception
    Try
        o = ps
    Catch ex2 As Exception
        Throw ex2
    End Try
End Try

and the result is a new exception

System.InvalidCastException: Unable to cast object of type 'System.Management.Automation.PSVariable' to type 'System.Management.Automation.PSVariable'.

LOL of all the world’s most lame error messages that has GOT to be the one.

Screw it. after playing around with a TON of different good/stupid/desperate/brilliant hacks I remember that I used SessionStateProxy.PSVariable.GetValue() when fetching single variables, so I try this approach instead, and it work’s .. Its ugly, I know, but what the hell am I/we suppose to do in a weird situation like this ?

Dim PSVar As System.Management.Automation.PSVariable
Try
    If host.IsRunspacePushed Then
        newPSVariableName = ps.Members("name").Value
        newPSVariableValue = ps.Members("value").Value
        newPSVariable = New PSVariable(newPSVariableName)

        PSVar = newPSVariable
        newPSVariable.Value = newPSVariableValue
    Else
        newPSVariable = ps.BaseObject
        PSVar = newPSVariable
        newPSVariableName = newPSVariable.Name
        newPSVariableValue = newPSVariable.Value
    End If
    If newPSVariableValue Is Nothing Then
    ElseIf InStr(newPSVariableValue.GetType.FullName, "System.Management.Automation") > 0 Then

        If newPSVariableValue.GetType.FullName = "System.Management.Automation.PSObject" Then
            Dim tmpPSObject As PSObject = newPSVariableValue
            newPSVariableValue = tmpPSObject.BaseObject
        Else
            newPSVariableValue = Nothing
        End If
    End If

    If newPSVariableValue Is Nothing Then
        newPSVariable = New PSVariable(newPSVariableName)
        newPSVariable.Value = newPSVariableValue
        PSVar = newPSVariable
    End If

mandag den 7. marts 2011

SCOM / mom certificates on non domain members

So I ‘m in the process of deploying SCOM 2007 R2 on several computers. Many of these are not members of the domain where SCOM is running and several of them are not even placed in our datacenter. I created a (I think) simple guide on how to install our root CA cert, request a computer certificate, install SCOM tell SCOM to use the certificate, patch it, and accept the client.

Apparently that isn't *that* easy again. The whole certificate thing was taking the most time, so I’ve looked into a way to automate this process. Playing around with different scenarios I stumbled across this blog post. Later while mocking up some code to facilitate it, I also stumbled across this post by same guy. I agree with what he's saying about where to generate key etc. but I was tasked with automating the process so here is my “modified” version. I stripped out authentication and various checks, to simplify it all, for anyone who wants to go down the same road as me. I run the PowerShell script he wrote (rewritten to VB.NET) then submit the BASE64 certificate request to a web service. I then send the request to our internal CA, and then send back the certificate. Then the VB.NET code installs the certificate in the local computer store.

First up, add a cert template according to he's guide here. Next a small console application that will be run at the client. You need to add a reference to certcli.dll and CertEnroll.dll

image

Then add this class

'  Add the CertEnroll namespace
Imports CERTENROLLLib
Imports CERTCLIENTLib
Imports System.Security.Cryptography.X509Certificates

Public Class SCOMCertWrapper

    Sub ImportPfxCertificate(ByVal filename As String, ByVal Store As StoreName, ByVal location As StoreLocation, ByVal password As String)
        Dim pfxcert As New X509Certificate2
        pfxcert.Import(filename, password, X509KeyStorageFlags.Exportable And X509KeyStorageFlags.PersistKeySet)
        Dim X509Store As New X509Store(Store, location)
        X509Store.Open(OpenFlags.MaxAllowed)
        X509Store.Add(pfxcert)
        X509Store.Close()
    End Sub

    Sub import509Certificate(ByVal filename As String, ByVal Store As StoreName, ByVal location As StoreLocation)
        Dim pfxcert As New X509Certificate2
        pfxcert.Import(filename)
        Dim X509Store As New X509Store(Store, location)
        X509Store.Open(OpenFlags.MaxAllowed)
        X509Store.Add(pfxcert)
        X509Store.Close()
    End Sub

    Function CreateRequest(ByVal DNSName As String, ByVal CertificateTemplate As String) As String
        ' create certificate Subject field in X500 Distinguished Name format
        Dim SubjectDN As New CX500DistinguishedName
        SubjectDN.Encode("CN=" & DNSName, X500NameFlags.XCN_CERT_NAME_STR_NONE)
        Dim OIDs As New CObjectIds
        ' add created OIDs to EnchancedKeyUsages certificate extension
        Dim OID As New CObjectId
        OID.InitializeFromValue("1.3.6.1.5.5.7.3.1")
        OID = New CObjectId
        OID.InitializeFromValue("1.3.6.1.5.5.7.3.2")
        OIDs.Add(OID)
        ' add created OIDs to EnchancedKeyUsages certificate extension
        Dim EKU As New CX509ExtensionEnhancedKeyUsage
        EKU.InitializeEncode(OIDs)

        ' generate private key
        Dim PrivateKey As New CX509PrivateKey
        PrivateKey.ProviderName = "Microsoft RSA SChannel Cryptographic Provider"
        ' the private key will be used by computer account
        PrivateKey.KeySpec = X509KeySpec.XCN_AT_KEYEXCHANGE
        ' the private key is supposed for Key Encipherment
        PrivateKey.KeyUsage = X509PrivateKeyUsageFlags.XCN_NCRYPT_ALLOW_ALL_USAGES
        PrivateKey.Length = 2048
        PrivateKey.MachineContext = True
        PrivateKey.Create()

        ' create certificate request template
        Dim PKCS10 As New CX509CertificateRequestPkcs10
        PKCS10.InitializeFromPrivateKey(X509CertificateEnrollmentContext.ContextMachine, PrivateKey, "")
        ' add necessary fields to certificate request template
        PKCS10.Subject = SubjectDN


        If CertificateTemplate <> "" Then
            Dim template As New CX509ExtensionTemplateName
            template.InitializeEncode(CertificateTemplate)
            PKCS10.X509Extensions.Add(template)
        Else
            PKCS10.X509Extensions.Add(EKU)
        End If

        ' generate request file

        Dim Request As New CX509Enrollment
        Request.InitializeFromRequest(PKCS10)
        ' certificate request will be saved in Base64 format with request header and footer

        'Dim Base64 As String = Request.CreateRequest(EncodingType.XCN_CRYPT_STRING_BASE64REQUESTHEADER)
        Dim Base64 As String = Request.CreateRequest(EncodingType.XCN_CRYPT_STRING_BASE64)
        Return Base64

        'strRequest = objEnroll.CreateRequest(EncodingType.XCN_CRYPT_STRING_BASE64)


    End Function

    Private Const CC_DEFAULTCONFIG As Integer = 0
    Private Const CC_UIPICKCONFIG As Integer = &H1
    Private Const CR_IN_BASE64 As Integer = &H1
    Private Const CR_IN_FORMATANY As Integer = 0
    Private Const CR_IN_PKCS10 As Integer = &H100
    Private Const CR_DISP_ISSUED As Integer = &H3
    Private Const CR_DISP_UNDER_SUBMISSION As Integer = &H5
    Private Const CR_OUT_BASE64 As Integer = &H1
    Private Const CR_OUT_CHAIN As Integer = &H100

    ' Submit request to CA and get response
    Public Function submitRequest(ByVal CA As String, ByVal base64 As String) As String
        '  Create all the objects that will be required
        Dim objCertConfig As CCertConfig = New CCertConfig()
        Dim objCertRequest As CCertRequest = New CCertRequest()
        Dim strCAConfig As String
        'Dim strRequest As String
        Dim iDisposition As Integer
        Dim strDisposition As String
        Dim strCert As String

        Try
            If CA = "" Then
                ' Get CA config from UI
                'strCAConfig = objCertConfig.GetConfig(CC_DEFAULTCONFIG);
                strCAConfig = objCertConfig.GetConfig(CC_UIPICKCONFIG)
            Else
                strCAConfig = CA ' "AD01.int.wingu.dk\int-AD01-CA"
            End If


            ' Submit the request
            iDisposition = objCertRequest.Submit(CR_IN_BASE64 Or CR_IN_FORMATANY, base64, Nothing, strCAConfig)

            ' Check the submission status
            If CR_DISP_ISSUED <> iDisposition Then
                ' Not enrolled
                strDisposition = objCertRequest.GetDispositionMessage()

                If CR_DISP_UNDER_SUBMISSION = iDisposition Then
                    ' Pending
                    Console.WriteLine("The submission is pending: " & strDisposition)
                    Return ""
                Else
                    Throw New Exception("The submission failed: " & strDisposition & vbCrLf & "Last status: " & objCertRequest.GetLastStatus().ToString())
                    ' Failed
                    Console.WriteLine("The submission failed: " & strDisposition)
                    Console.WriteLine("Last status: " & objCertRequest.GetLastStatus().ToString())
                    Return ""
                End If
            End If

            ' Get the certificate
            strCert = objCertRequest.GetCertificate(CR_OUT_BASE64 Or CR_OUT_CHAIN)

            Return strCert
        Catch ex As Exception
            Throw ex
            Console.WriteLine(ex.Message)
        End Try
    End Function

    ' Install response from CA
    Public Sub accept(ByVal BASE64 As String)
        '  Create all the objects that will be required
        Dim objEnroll As CX509Enrollment = New CX509Enrollment()
        'Dim strCert As String

        Try
            'strCert = responseTextText

            ' Install the certificate
            objEnroll.Initialize(X509CertificateEnrollmentContext.ContextMachine)
            objEnroll.InstallResponse(InstallResponseRestrictionFlags.AllowUntrustedRoot, BASE64, EncodingType.XCN_CRYPT_STRING_BASE64, Nothing)

            Console.WriteLine("Certificate installed!")
        Catch ex As Exception
            Throw ex
            Console.WriteLine(ex.Message)
        End Try
    End Sub

End Class

In main() add

Imports System.Security.Cryptography.X509Certificates

Module Module1

    Function myFQDN() As String
        Dim fqdn As String
        Dim domainName As String = Net.NetworkInformation.IPGlobalProperties.GetIPGlobalProperties().DomainName
        Dim hostname As String = Net.Dns.GetHostName()
        If Not hostname.Contains(domainName) Then
            fqdn = hostname & "." & domainName
        Else
            fqdn = hostname
        End If
        Return fqdn.ToLower
    End Function

    Sub Main()
        Dim wsClientCertificate As New ClientCertificate.ClientCertificate
        Dim fqdn As String = myFQDN()
        Console.WriteLine("My hostname is: " & fqdn)

        Console.WriteLine("Downloading Root CA certificate")
        Dim webcli As New Net.WebClient
        webcli.DownloadFile("https://somedomain/certnew.cer", "certnew.cer")

        Console.WriteLine("Importing Root CA certificate to Trusted Root Certificate Authorities ")
        Dim cer As New SCOMCertWrapper
        cer.import509Certificate("certnew.cer", StoreName.AuthRoot, StoreLocation.LocalMachine)

        Console.WriteLine("Creating a new certificate request for local computer: " & fqdn)
        Dim base64req As String = cer.CreateRequest(fqdn, "OpsMgrAgentV2")
        Console.WriteLine("Submitting certificate request to webservice.")
        Dim base64cer As String = wsClientCertificate.submitRequest(base64req)
        Console.WriteLine("importing certificate to local machine personal certificates")
        cer.accept(base64cer)


        Console.WriteLine("done.")
        Console.ReadLine()

    End Sub

End Module

create a web service, add references like in console application. place root CA somewhere accessible, and also add the class ScomCertWrapper

<WebMethod()> _
Public Function submitRequest(ByVal base64_request As String) As String
    Dim cert As New MomCert
    Dim base64_cert As String = cert.submitRequest("fqnd.of.your.CA\int-AD01-CA", base64_request)
    Return base64_cert
End Function

If your really lazy you can also start a process in the client calling MOMCertImport.exe Smile

I properly don’t need to point this out, but I’ll do it anyway. You need to impersonate a (or run the application pool as a ) user who have permission to Enroll certificates based on the template, you should also add some kind authentication on the web service.

If you don’t know what to type as parameter 1 to submitRequest. Set a line break at line 105 in the class, call it from a console application, without specifying CA and choose it from the list that will popup, then inspect strCAConfig and you’ll know what to supply.

Update: so testing this code, I started getting "The requested certificate template is not supported by this CA. 0x80094800". The error was generated at InitializeFromPrivateKey while doing the creating of the Certificate request. Turns out that the last parameter CertificateTemplate requires the Template to be in the clients cache. Well that will never happen we are doing this from non-domain members or foreign domain members. Just set the parameter to an empty string or Nothing and it will work, but then you need to remember to add the template name to the request. If you don’t supply either you will get “Denied by Policy Module  0x80094801, The request does not contain a certificate template extension or the CertificateTemplate request attribute.” when submitting the request to the CA.

fredag den 4. marts 2011

Creating a windows service that runs as a console too

 

Someone asked me to create a step by step guide on how to make a windows service that is easy to debug too. So here it is. You can do this in two ways. Either start by choosing the Windows Service template, chance the Application Type under MY Project –> Application to “Console Application” then select “Sub Main” in Startup Object. Add a Module, and add a function main to that. After that you can add a project installer under Service Class. Or if you want a bit more control you can start by creating a Windows Forms Application or Console Application and then add the Service things.

Create a Windows Application.

image

Add a reference to System.ServiceProcess and System.Configuration.Install

image

 

Add a class called ServiceManager with the following code

Public Class ServiceManager

    Private _serviceName As String
    Public Const PROCESSBASICINFORMATION As UInteger = 0

    <System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential, Pack:=1)> _
    Public Structure Process_Basic_Information
        Public ExitStatus As IntPtr
        Public PepBaseAddress As IntPtr
        Public AffinityMask As IntPtr
        Public BasePriority As IntPtr
        Public UniqueProcessID As IntPtr
        Public InheritedFromUniqueProcessId As IntPtr
    End Structure

    <System.Runtime.InteropServices.DllImport("ntdll.dll", EntryPoint:="NtQueryInformationProcess")> _
    Public Shared Function NtQueryInformationProcess(ByVal handle As IntPtr, ByVal processinformationclass As UInteger, ByRef ProcessInformation As Process_Basic_Information, ByVal ProcessInformationLength As Integer, ByRef ReturnLength As UInteger) As Integer
    End Function

    Sub New(ByVal serviceName As String)
        _serviceName = serviceName
    End Sub

    Public ReadOnly Property status() As System.ServiceProcess.ServiceControllerStatus
        Get
            Using serviceController As New System.ServiceProcess.ServiceController(_serviceName)
                If Not serviceController Is Nothing Then
                    Try
                        Return serviceController.Status
                    Catch ex As Exception
                        Return ServiceProcess.ServiceControllerStatus.Stopped
                    End Try
                Else
                    Return ServiceProcess.ServiceControllerStatus.Stopped
                End If
            End Using
        End Get
    End Property

    Function IsServiceInstalled() As Boolean
        Using serviceController As New System.ServiceProcess.ServiceController(_serviceName)
            Try
                Dim status As System.ServiceProcess.ServiceControllerStatus = serviceController.Status
            Catch generatedExceptionName As InvalidOperationException
                Return False
            Catch ex As Exception
                EventLog.WriteEntry("ServiceManager", ex.ToString(), EventLogEntryType.[Error])
                Return False
            End Try
            Return True
        End Using
    End Function

    Private Function GetAssemblyInstaller(ByVal commandLine As String()) As System.Configuration.Install.AssemblyInstaller
        Dim installer As New System.Configuration.Install.AssemblyInstaller()
        installer.Path = System.Reflection.Assembly.GetExecutingAssembly().Location
        installer.CommandLine = commandLine
        installer.UseNewContext = True
        Return installer
    End Function

    Sub UninstallService()
        If Not IsServiceInstalled() Then
            Exit Sub
        End If
        Dim commandLine As String() = New String(0) {}
        commandLine(0) = System.Reflection.Assembly.GetExecutingAssembly().Location
        Dim mySavedState As IDictionary = New Hashtable()
        mySavedState.Clear()
        Dim installer As System.Configuration.Install.AssemblyInstaller = GetAssemblyInstaller(commandLine)
        Try
            installer.Uninstall(mySavedState)
        Catch ex As Exception
            EventLog.WriteEntry("ServiceManager", ex.ToString(), EventLogEntryType.[Error])
        End Try
    End Sub

    Sub InstallService()
        If IsServiceInstalled() Then
            Exit Sub
        End If

        Try
            Dim commandLine As String() = New String(0) {}
            commandLine(0) = System.Reflection.Assembly.GetExecutingAssembly().Location
            Dim mySavedState As IDictionary = New Hashtable()
            Dim installer As System.Configuration.Install.AssemblyInstaller = GetAssemblyInstaller(commandLine)
            Try
                installer.Install(mySavedState)
                installer.Commit(mySavedState)
            Catch ex As Exception
                installer.Rollback(mySavedState)
                EventLog.WriteEntry("ServiceManager", ex.ToString(), EventLogEntryType.[Error])
            End Try
        Catch ex As Exception
            EventLog.WriteEntry("ServiceManager", ex.ToString())
        End Try
    End Sub

    Sub StartService()
        If Not IsServiceInstalled() Then Exit Sub
        Using serviceController As New System.ServiceProcess.ServiceController(_serviceName)
            If serviceController.Status = ServiceProcess.ServiceControllerStatus.Stopped Then
                Try
                    serviceController.Start()
                    WaitForStatusChange(serviceController, ServiceProcess.ServiceControllerStatus.Running)
                Catch ex As InvalidOperationException
                    EventLog.WriteEntry("ServiceManager", ex.ToString(), EventLogEntryType.[Error])
                End Try
            End If
        End Using
    End Sub

    Sub StopService()
        If Not IsServiceInstalled() Then Exit Sub
        Using serviceController As New System.ServiceProcess.ServiceController(_serviceName)
            If serviceController.Status <> ServiceProcess.ServiceControllerStatus.Running Then
                Exit Sub
            End If

            serviceController.Stop()
            WaitForStatusChange(serviceController, ServiceProcess.ServiceControllerStatus.Stopped)
        End Using
    End Sub

    Private Sub WaitForStatusChange(ByVal serviceController As System.ServiceProcess.ServiceController, ByVal newStatus As System.ServiceProcess.ServiceControllerStatus)
        If Not IsServiceInstalled() Then Exit Sub
        Dim count As Integer = 0
        While serviceController.Status <> newStatus AndAlso count < 30
            System.Threading.Thread.Sleep(1000)
            serviceController.Refresh()
            count += 1
        End While

        If serviceController.Status <> newStatus Then
            Throw New Exception("Failed to change status of service. New status: " & newStatus)
        End If
    End Sub

    Public Shared Function getParentProcess(ByVal Process As Process) As Process
        Dim pi As New Process_Basic_Information
        Dim RetLength As UInteger
        NtQueryInformationProcess(Process.Handle, PROCESSBASICINFORMATION, pi, Runtime.InteropServices.Marshal.SizeOf(pi), RetLength)
        Return Process.GetProcessById(pi.InheritedFromUniqueProcessId)
    End Function
End Class

Add a new Class and call it something meaningful. This will be where you onStart onStop functions will be. I called it TestServiceControl

image

Add the following code to the Class

Public Class TestServiceControl
    Inherits System.ServiceProcess.ServiceBase

    Sub New()
        Me.ServiceName = "TestService"
    End Sub

    Protected Overrides Sub OnStart(ByVal args() As String)
        Try
            isStopping = False
            Dim ts As System.Threading.ThreadStart
            ts = AddressOf mainModule.doWork
            workerThread = New System.Threading.Thread(ts)
            workerThread.Start()
            'Me.CanStop = True
        Catch ex As Exception
            Log("TestService.OnStart: " & ex.ToString, EventLogEntryType.Error)
        End Try
    End Sub

    Protected Overrides Sub OnStop()
        Try
            isStopping = True
            ' stall for 10 seconds to let work finish
            For i As Integer = 0 To 10
                Threading.Thread.Sleep(1000)
                If Not workerThread.IsAlive Then Exit For
            Next
            ' Kill it if not done yet
            If workerThread.IsAlive Then workerThread.Abort()
        Catch ex As Exception
            Log("TestService.OnStop: " & ex.ToString, EventLogEntryType.Error)
        End Try
    End Sub

End Class

<System.ComponentModel.RunInstallerAttribute(True)> _
Public Class TestServiceControlInstaller
    Inherits System.Configuration.Install.Installer

    Friend WithEvents myServiceProcessInstaller As New System.ServiceProcess.ServiceProcessInstaller
    Friend WithEvents myServiceInstaller As New System.ServiceProcess.ServiceInstaller

    Sub New()
        myServiceProcessInstaller.Account = System.ServiceProcess.ServiceAccount.LocalSystem
        myServiceProcessInstaller.Password = Nothing
        myServiceProcessInstaller.Username = Nothing
        myServiceInstaller.Description = "Test Service"
        myServiceInstaller.DisplayName = "Test Service"
        myServiceInstaller.ServiceName = "TestService"
        myServiceInstaller.StartType = System.ServiceProcess.ServiceStartMode.Automatic

        Me.Installers.AddRange(New System.Configuration.Install.Installer() {myServiceProcessInstaller, myServiceInstaller})
    End Sub

    Private Sub ServiceProcessInstaller1_BeforeInstall(ByVal sender As Object, ByVal e As System.Configuration.Install.InstallEventArgs) Handles myServiceProcessInstaller.BeforeInstall
        Try
            ' examle of how to dyamicly set what service should run as.
            ' you can find the code to some of these functions else where on my blog
            'If _MachineMemberOfDomain() Then
            'LsaUtility.SetRight(InstallerAccount, "SeServiceLogonRight")
            'AddUserToLocalAdministrators(InstallerDomain & "\" & InstallerAccount)
            'ServiceProcessInstaller1.Account = ServiceProcess.ServiceAccount.User
            'ServiceProcessInstaller1.Username = InstallerDomain & "\" & InstallerAccount
            'ServiceProcessInstaller1.Password = InstallerPassword
            'Else
            'ServiceProcessInstaller1.Account = ServiceProcess.ServiceAccount.LocalSystem
            'End If
        Catch ex As Exception
            Log("ProjectInstaller: " & ex.ToString, EventLogEntryType.Error)
            Throw New Exception(ex.Message, ex)
        End Try
    End Sub

End Class

 

Rename Module1 to something meaningful (I called it mainModule ), and add the following code.

Module mainModule

    Friend myManager As New ServiceManager("TestService")
    Public isStopping As Boolean = False
    Public workerThread As System.Threading.Thread
    Public isService As Boolean = False

    Sub Main()
        Try
            ' Uncomment the following like if you need to attach as a debuger to the windows service
            Dim ParentProcess As Process = ServiceManager.getParentProcess(Process.GetCurrentProcess)
            isService = (ParentProcess.ProcessName.ToLower = "services")
            If My.Application.CommandLineArgs.Count > 0 Then
                Dim arg1 As String = My.Application.CommandLineArgs(0).ToLower
                If arg1 = "-u" Or arg1 = "-uninstall" Or arg1 = "/u" Or arg1 = "/uninstall" Then
                    If myManager.IsServiceInstalled Then
                        myManager.UninstallService()
                    Else
                        Console.WriteLine("im not installed as a service.")
                    End If
                    Exit Sub
                End If
            End If
            ' Some might prefere using /i instead. You can also start the service after using myManager.StartService
            If Not myManager.IsServiceInstalled Then
                Try
                    myManager.InstallService()
                Catch ex As Exception
                    Throw ex
                End Try
            End If
            If isService Then
                Try
                    System.ServiceProcess.ServiceBase.Run(New TestServiceControl)
                Catch ex As Exception
                    'screw it, guess im running as console then.
                    isService = False
                End Try
            Else
                doWork()
            End If
            If Debugger.IsAttached Then
                Console.WriteLine("Process has ended. Press enter to exit.")
                Console.ReadLine()
            End If
        Catch ex As Exception
            Log("mainModule.Main " & ex.ToString, EventLogEntryType.Error)
        End Try
    End Sub

    Public Sub Log(ByVal msg As String, ByVal msgType As System.Diagnostics.EventLogEntryType)
        Try
            If isService Then
                System.Diagnostics.EventLog.WriteEntry("TestService", msg, msgType)
            Else
                Console.WriteLine("[" & Now.Hour & ":" & Now.Minute & "." & Now.Second & "] " & msg)
            End If
        Catch ex As Exception
        End Try
    End Sub

    Public Sub doWork()
        Try
            ' Add all you initializing code here. Dont use Main. The Service controler dont like waiting on Start Controls
            ' A cute trick here is closing gracefullt using stopMyself() in case that is needed. You can do that pretty much anywhere
            While Not isStopping
                ' Do what ever you need to do as a service



                If Not isService Then
                    isStopping = True
                Else
                    Threading.Thread.Sleep(10000)
                End If
            End While
        Catch ex As System.Threading.ThreadAbortException
            ' this is known, ignore it ( TestServiceControl.OnStop is killing me )
        Catch ex As Exception
            Log("mMain.doWork: MainLoop cought an unhandled exception: " & ex.ToString, EventLogEntryType.Error)
        End Try
    End Sub

    Sub stopMyself()
        Log("mMain.stopMyself: Forcing my self to stop (CloudInstallerService)", EventLogEntryType.Information)
        If isService Then
            Try
                myManager.StopService()
            Catch ex As Exception
                Log("mMain.stopMyself: " & ex.ToString, EventLogEntryType.Error)
                isStopping = True
            End Try
        Else
            isStopping = True
        End If
    End Sub

End Module

And that’s it. You can now more rapidly work with a Windows Service.
You can download this as a Microsoft Visual Studio 2010 solution here.