Call method from out-of-process COM component using PHP on Azure - php
I've written a simple PHP script which instantiates a COM object for an out-of-process (i.e. exe file) COM component and uses it to call a COM method that the component exposes. This COM method very simply quadruples the number passed as the first argument, returning the result in the second argument (passed by reference). The script shown below works successfully on my local development machine on WampServer 2.0 (Apache 2.2.11 / PHP 5.3.1). The COM component is a Win32 executable built using Delphi.
<?php
// ensure no time limit is imposed
set_time_limit(0);
// show all errors, warnings and notices whilst developing
error_reporting(E_ALL);
$numIn = 3;
$numOut = new VARIANT(1, VT_I4);
echo '----- BEFORE ---------' . '<br>';
echo 'NumIn: ' . $numIn . '<br>';
echo 'NumOut: ' . $numOut . '<br>';
echo '----------------------' . '<br>';
$oleapp = new COM("OleAutomationFeasibilityModel.Automation") or die ("Could not initialise feasibility model object.");
echo '<br />COM object created version = ' . $oleapp->Version . '<br /><br />';
$oleapp->CalculateWithVariants($numIn, $numOut);
unset($oleapp);
echo '----- AFTER ---------' . '<br>';
echo 'NumIn: ' . $numIn . '<br>';
echo 'NumOut: ' . $numOut . '<br>';
echo '----------------------' . '<br>';
?>
Note: as I understand it, one can only pass a parameter by reference to a COM method using a VARIANT type, as common data types like integers and strings won't work (see http://www.php.net/manual/en/ref.com.php#45038).
I then created and deployed an Azure Web Role (Cloud Service) with a startup script that registers the COM component successfully i.e. the appropriate registry keys appeared in the registry. To further confirm that the COM component could be interacted with, I used RDP to connect to the cloud service instance and installed Microsoft Access Runtime 2010 as I have an Access application that provides a GUI to test the methods of the COM component. I was able to run this application and successfully interacted with the COM component, using it to pass an integer to the CalculateWithVariants method and the expected quadrupled result was returned. So, I've established that the COM component is installed and can be interacted with on the Azure cloud service instance.
Next I included the above PHP script in the Web Role and deployed it on Azure. Unfortunately, calling the script from a browser results in an HTTP Error 500 (Internal Server Error) and I'm struggling to find out why. If I comment out all lines referencing $oleapp, I still get the same error. If I additionally comment out the line that instantiates a variant object, no error occurs. If I reinstate the line which instantiates the COM object and the line below it, I receive no error message but the only text echoed is from the lines preceding the COM object creation line i.e. the call to the Version method fails. So it appears to be struggling with the variant object creation and the COM object creation.
I'm a bit stuck in terms of how to resolve this issue. I would therefore be very grateful if anyone has any pointers as to a way forward.
UPDATE 1
I decided to try a different course of action on the Azure platform by...
creating an Azure Virtual Machine with a Windows Server 2008 R2 OS
installing WampServer 2.2E (Apache 2.2.22 / PHP 5.3.13 / MySQL
5.5.24) in the VM as a quick and easy way to test whether this approach would work
copying the above PHP script into the WampServer "www directory"
launching WampServer
selecting the "Put Online" option from the WampServer Menu (accessed by left-clicking the WampServer icon in the Windows Taskbar Notification area)
creating an "Inbound Rule" for the VM firewall to allow connections to port 80
...and thankfully the script ran successfully!
Ideally, I would still like to get this working as an Azure cloud service as it shouldn't be necessary for me to maintain the PHP installation in a full VM.
UPDATE 2
I tried restarting the cloud service, then remotely connecting to an instance of the cloud service and looking in the Application Event Viewer. I saw that WMI logged 1 error during startup:
Event filter with query "SELECT * FROM __InstanceModificationEvent WITHIN 60
WHERE TargetInstance ISA "Win32_Processor" AND TargetInstance.LoadPercentage > 99"
could not be reactivated in namespace "//./root/CIMV2" because of error 0x80041003
Events cannot be delivered through this filter until the problem is corrected.
I then ran the above script a couple of times and rechecked the Application Event Viewer but nothing had been logged.
I also checked the IIS logs and the Azure log, startup-tasks-log and startup-tasks-error-log files to no avail.
After giving up on solving this last year. I made another concerted effort to resolve it this week and succeeded!
I basically needed to (a) enable the php_com_dotnet.dll to allow use of COM and VARIANT classes, and (b) grant default Local Activation permission to IIS_IUSRS to allow access to the COM component. I've listed the detailed steps I took below...
Add a folder called php in the web role's bin folder
As of PHP 5.3.15 / 5.4.5, in order to use the COM and VARIANT
classes, the php_com_dotnet.dll needs to be enabled inside of
php.ini. Previous versions of PHP enabled these extensions by
default (source: http://www.php.net/manual/en/com.installation.php). In
the php folder, create a php.ini file containing only the following
lines...
[COM_DOT_NET]
extension=php_com_dotnet.dll
Create a SetDCOMPermission.reg file in the bin folder which contains the following content, to grant default Local Activation permission to IIS_IUSRS...
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\OLE]
"DefaultLaunchPermission"=hex(3):01,00,04,80,74,00,00,00,84,00,00,00,00,00,\
00,00,14,00,00,00,02,00,60,00,04,00,00,00,00,00,14,00,1F,00,00,00,01,01,00,\
00,00,00,00,05,12,00,00,00,00,00,18,00,1F,00,00,00,01,02,00,00,00,00,00,05,\
20,00,00,00,20,02,00,00,00,00,18,00,0B,00,00,00,01,02,00,00,00,00,00,05,20,\
00,00,00,38,02,00,00,00,00,14,00,1F,00,00,00,01,01,00,00,00,00,00,05,04,00,\
00,00,01,02,00,00,00,00,00,05,20,00,00,00,20,02,00,00,01,02,00,00,00,00,00,\
05,20,00,00,00,20,02,00,00
I don't know if the above registry change will work for everyone, so the process I used is documented here (it essentially involved using a program called RegFromApp to record the changes made to the registry when granting default Local Activation permissions for IIS_IUSRS in COM Security and to save the registry changes as a .reg file into the web role's bin folder).
Copy and paste the out-of-process COM component (OleAutomationFeasibilityModel.exe file) into the bin folder
Create a RegisterOleAutomationFeasibilityModel.cmd file in the bin folder to register the COM component and set the necessary permissions to launch it...
chcp 1252>NUL
OleAutomationFeasibilityModel.exe /regserver
regedit.exe /s SetDCOMPermission.reg
exit /b 0
In the ServiceDefinition.csdef file, insert a reference to the .cmd file immediately before the closing Startup tag...
<Task commandLine="RegisterOleAutomationFeasibilityModel.cmd" executionContext="elevated" />
Publish the web role
Hope that helps someone in a similar situation!
Related
How to connect to a WebSphere MQ Queue using SSL authentification in PHP
I want to connect to a WebSphere MQ Queue using SSL authentification from a PHP application (script). Queue Manager Version is 7.0 or 7.5 MQ Client Version is 8.0 PHP Version is 7.0 (docker) Using PHP mqseries pecl extension v0.15 (with custom fixes) So far I was able to connect without SSL connection. Here are the steps I have to done so far: I have installed the WebSphere MQ Client v8 for Linux (CentOS in my case) I have downloaded the PECL 0.15 version of the mqseries php extension. (There as a small bug in the extension, I had to recompile it to make it work properly. I used to get a segmentation fault on MQGET). I linked the the mqseries.so to php and enabled the extension. I succesfully (without SSL) Connect to the queue manager Open the queue for reading Get messages on the queue Close the connection When I set the USE_SSL in my script to true, I get the error code 2393 that means "An MQCONN or MQCONNX call was issued with SSL configuration options specified, but an error occurred during the initialization of the SSL environment." This error message is very generic and does not help me pin point where is the problem. The MQ_KEYSTORE is set as /path/to/my/key and my filename is key.kdb and has at the same level key.sth as suggested by this documentation The MQ_SSL_CIPHER_SPEC is the same in the script than specified by on the queue manager for the specified MQ_CHANNEL_SSL. Checked multiple times. These are NOT the Cipher Suite used with JMS connections The security cache has been refreshed on the queue manager. On the server side, I checked the error logs for the queue manager and didn't seem to see my channel name. I say "seem" because there was a lot of noise and there were a few ??? channel name in the lot. So I feel like it did not reach the queue manager for some reason. I also used the "amqssslc" command found in the MQ Client installation bin folder to test my ssl config. I get the same error than using the PHP script. I also used WireShark to sniff packets on the corresponding MQ_PORT. The content of the packages contained certificate information. So there is something that looks like a SSL hand shake going on. I am now out of ideas as of how to debug the case. Does anyone has an idea of what to check next? Is there connection logs on my MQ Client installation that I should check? Here is an example of a connection using SSL in PHP Here is a simplified version of my MQ script (I removed the outputs). Some of the constants are not disclosed for security purposes. All MQSERIES_* constants are defined in the extension All MQ_* are hardcoded parameters to test my script but their definition does not appear in the script excerpt. <?php // Constants defined here $options = [ 'Version' => MQSERIES_MQCNO_VERSION_4, 'Options' => MQSERIES_MQCNO_STANDARD_BINDING, 'MQCD' => [ 'Version' => 7, 'ChannelName' => MQ_CHANNEL, 'ConnectionName' => sprintf('%s(%s)', MQ_HOST, MQ_PORT), 'TransportType' => MQSERIES_MQXPT_TCP, ] ]; if (USE_SSL) { $options['MQSCO'] = [ 'KeyRepository' => MQ_KEYSTORE ]; $options['MQCD']['ChannelName'] = MQ_CHANNEL_SSL; $options['MQCD']['SSLCipherSpec'] = MQ_SSL_CIPHER_SPEC; } mqseries_connx(MQ_QUEUE_MANAGER, $options, $conn, $comp_code, $reason ); $mqods2 = [ 'ObjectType' => MQSERIES_MQOT_Q, 'ObjectName' => MQ_QUEUE ]; mqseries_open($conn, $mqods2, MQSERIES_MQOO_INPUT_AS_Q_DEF | MQSERIES_MQOO_FAIL_IF_QUIESCING, $obj, $comp_code, $reason); $gmd = []; $gmo = [ 'Options' => MQSERIES_MQGMO_FAIL_IF_QUIESCING | MQSERIES_MQGMO_WAIT, 'WaitInterval' => 3000 ]; $msg = ""; $data_length = ""; for ($i = 0; $i < 1000; $i++) { mqseries_get($conn, $obj, $gmd, $gmo, 10000, $msg, $data_length, $comp_code, $reason); if ($reason === 2033) { printf("No more messages to process\n"); break; } // Business logic } mqseries_disc($conn, $comp_code, $reason); ?> UPDATE Using the client side logs that I didn't know existed. We finally found out that our server was not using a proper certificate. The server's certificate was actually self-signed, thus not granting us access because our .kdb file didn't have it's public key. We added the server's own public key to the .kdb file making that step work out. As suggested in the comments from JoshMC, for our next problem, we suspect the label is wrong in the keystore file. We did not specify a specific label from the .kdb file. From the docs "For queue managers and clients respectively, the following sources are searched in sequence for a non-empty value. The first non-empty value determines the certificate label. The certificate label must exist in the key repository. If no matching certificate in the correct case and format is found that matches a label, an error occurs and the SSL or TLS handshake fails." And it also states that "The certificate label cannot contain spaces.". I will try again tomorrow with proper label naming and sending a specific label name. I will try to see if the name convention ibmwebspheremq<user_that_runs_the_php_process> actually impacts the validity. UPDATE 2 It finally worked out. As mentioned by JoshMC, the private key needs to have the specific label ibmwebspheremq. I didn't try the CertificateLabel yet. I might dig into that next week. At first we were using the GUI (ikeyman) from ibm to generate the .kdb file. But we found out it had quite a few bugs in it. For instance, it imports twice the private key with the same label (auto-generated label from the certificate). Editing the label name was not possible (hence the connection problems). To solve that part we used the command line tool ikeycmd that basically offers the same features on the command line (minus the bugs). To run the command, you need the IBM jre (Not verified, needs to be checked out) and run it as java com.ibm.gsk.ikeyman.ikeycmd. From here, there is a whole world of documentation on IBM web site about how to create a certificate, rename labels, check details, etc. Lots and lots of fun!
With a full IBM MQ client install, client side errors will be logged to the directory: /var/mqm/errors The errors if any will be logged to the file AMQERR01.LOG (this gets rotated with two other files ending in 02 and 03. If the error is something that MQ is not expecting it may also create a file that ended in .FDC with additional details. The Queue Manager will log a channel name of ??? if the connection has failed during the channel negotiating prior to the client the sending the channel name. If the IBM MQ the queue manager is using v7.5 or lower the channel name is not exchanged until after the TLS handshake is complete. With v8 and later clients connecting to v8 and later queue managers, MQ will use TLS SNI to exchange the channel name during TLS negotiation, however I am unsure if MQ has also been enhanced to log this channel name where in prior versions it logged ???. Note that IBM MQ Classes for Java and IBM MQ Classes for JMS client even at v8 and later do not support the SNI feature and do not send the channel name until after the TLS handshake is complete. Check that your client key.kdb has a private key. There are three ways which MQ uses to identify which private key to use. With MQ Client v7.5 and earlier the label of the private key must be: ibmwebspheremq<user_that_runs_the_php_process> With MQ client v7.5 and earlier an alternative is to set the private key as the default certificate and make sure the following environment variable is set in the environment which executes the program. Note this also works with 8.0.0.7/9.0.0.1 but I prefer the next method over this one. AMQ_SSL_ALLOW_DEFAULT_CERT=1 With MQ Client v8 and later you can use CertificateLabel=anylabelvalue in the SSL stanza of the mqclient.ini. Example below: SSL: CertificateLabel=anylabelvalue The CertificateLabel setting is documented in the IBM MQ v8 KC page "SSL stanza of the client configuration file". A template for mqclient.ini can be copied from /var/mqm/mqclient.ini and placed in the same directory where the application executes. The location can also be specified via a environment variable and MQ will look in a few other locations for the file if it the variable is not set and it does not find it in the same directory where the application executes. The various ways in which MQ will look for this file is documented in the IBM MQ v8 KC page "Location of the client configuration file"
Joomla add article via php script Application Instantiation Error
i created a script in order to insert some articles via a php script. This script is working on my local machine (xampp), but when i deploy the whole joomla project to my web server i get the following error message: Error displaying the error page: Application Instantiation Error: Application Instantiation Error By adding some echo calls, i was able to find the line which causes the error: $app = JFactory::getApplication('site'); Now i am wondering how to fix this behaviour and make my function also run on the web server. Below i will provide my system informations and the beginning of my php function until the line, which causes the error message: Systeminformations joomla version: 3.6.5 Stable db version: 5.6.34-79.1-log php version: 5.6.30-he.0 web server: Apache PHP snippet <?php echo "STARTING; "; // get db connection include('../includes/mysql.inc.php'); // get all sql querys include('./autoNewsQuerys.inc.php'); /** * Prepare joomla framework to insert article correctly */ if (!defined('_JEXEC')) { define('_JEXEC', 1); define('JPATH_BASE','/is/htdocs/wp1088688_4E1H7PYJFK/www'); require_once(JPATH_BASE . '/includes/defines.php'); require_once(JPATH_BASE . '/includes/framework.php'); defined('DS') or define('DS', DIRECTORY_SEPARATOR); } echo "searching app;"; $app = JFactory::getApplication('site'); echo "Found app;"; Full browser output STARTING; searching app;Error displaying the error page: Application Instantiation Error: Failed to start the session because headers have already been sent by "/is/htdocs/wp1088688_4E1H7PYJFK/www/1500AutoNews/autoNews.php" at line 3. So as you can see the error is caused by the JFactory call. 'Found app' is never printed out.
Just copy the Joomla index.php file to another file and then replace the $app->execute code with your code. Note: You don't need to include any MySQL library - since you are loading the Joomla environment then you have access to the JDatabase class, which is a database abstraction layer. Another note: this file autoNewsQuerys.inc.php likely contains some issues (maybe you are trying to instantiate the app there, but the Joomla environment hasn't loaded yet). You should move it to the end.
if it was working on localhost and it is not working on Web server then the biggest issue is with defining the base path. Give a hardcoded base path of your joomla installation. I suggest you to check your base path using a php script. Create a php file and name it path.php. Put it in the Joomla main folder. Content of the file <?php echo "The Base path is ".getcwd(); ?> Once you get the path just change the Base_Path. For ex: define('JPATH_BASE', '\var\www\joomla');
How to run COM objects on a remote server in PHP
How can I run a COM object that is located in a dll file on a remote server? According to php.net (http://php.net/manual/en/faq.com.php#faq.com.q8): How can I run COM object from remote server ? Exactly like you run local objects. You only have to pass the IP of the remote machine as second parameter to the COM constructor. Make sure that you have set com.allow_dcom=TRUE in your php.ini. I have com.allow_dcom enabled in my php.ini and according to phpinfo(); I do in fact have COM support, DCOM support, and .NET support enabled. I am having a hard time finding examples of how to call the remote objects. The DLL file (pcmsrv32.dll) is located in C:\Windows on a remote server. I need to access the object method CalcDistance() which is stored in that file. I have tried to pass the file location and the IP to the COM class: $obj = new COM("C:\Windows\PCMSRV32.DLL","10.86.0.21"); But that does not work. I get this error: Fatal error: Uncaught com_exception: Failed to create COM object `C:\Windows\PCMSRV32.DLL': Moniker cannot open file in C:\Users\...\index.php:33 I have also tried using the ProgID given in the User Guide for PC*Miler|Connect and used that in my code: $com = new COM("PCMServer.PCMServer.1","10.86.0.21"); However that gives me this error: com_exception: Failed to create COM object `PCMServer.PCMServer.1': Invalid syntax What am I doing wrong?
I ended up getting in touch with PC*Miler Support, and they gave me a sample code for making a basic call that can be run as a CLI script: <?php // Create COM Object $pcms = new COM("PCMServer.PCMServer"); // Calculate $dist = $pcms->CalcDistance("12345","23456"); // Returned distance is a INT that needs to be divided by 10 (or the number of decimal places specified in the PCMSServe.ini file located in C:\Windows $properDist = ($dist/10); echo $properDist ?> EDIT I am now being told that PC*Miler only offers their COM objects locally. So unless I have it installed on the same server that I am developing on, this will not work. We are installing version 30 of PC*Miler to the same server where my PHP code resides and trying that instead. UPDATE We had v.30 of PC*Miler installed on the same server, as well as the patch that fixes a php bug, and I was able to successfully run the following code: try { $pcms = new COM("PCMServer.PCMServer"); } catch (com_exception $e) { print $e . "\n"; } // Calculate $dist = $pcms->CalcDistance("Goodyear, AZ","Las Vegas, NV"); $properDist = ($dist/10); echo $properDist; Output of $properDist was 288 miles - which I verified was correct with Google Maps.
Problems using SAPI with PHP through COM, on IIS
I am attempting to get Text To Speech and Speech Recognition to work in PHP using Microsoft's SAPI through COM objects. In the past, I already used this code to get TTS to work (Apache 2.1, PHP 5.5, on Windows 2003 Server) // Instantiate the object $VoiceObj = new COM("SAPI.SpVoice") or die("Unable to instantiate SAPI"); // Get the available voices $VoicesToken=$VoiceObj->GetVoices(); $NumberofVoices=$VoicesToken->Count; for($i=0;$i<$NumberofVoices;$i++) { $VoiceToken=$VoicesToken->Item($i); $VoiceName[$i]=$VoiceToken->GetDescription(); } // Get and print the id of the specified voice $SelectedVoiceToken=$VoicesToken->Item(0); $SelectedVoiceTokenid=$SelectedVoiceToken->id; // Set the Voice $VoiceObj->Voice=$SelectedVoiceToken; $VoiceName=$VoiceObj->Voice->GetDescription(); $VoiceFile = new COM("SAPI.SpFileStream"); $VoiceFile->Open('./test.wav', 3, false); // Speak to file $VoiceObj->AudioOutputStream = $VoiceFile; $VoiceObj->Speak("What an unbelievable test", 0); $VoiceFile->Close(); On my new setup (IIS 7.5, PHP 7.0, Windows Server 2008R2) the same code fails at $VoiceObj->Speak("What an unbelievable test", 0); Fatal error: Uncaught com_exception: <b>Source:</b> Unknown<br/><b>Description:</b> Unknown in \\web\tts.php:30 Stack trace: #0 \\web\tts.php(30): com->Speak('this is a marve...', 0) #1 {main} With such little detail (where to retrieve more?) I can't figure out what the problem may be. Writing permissions checked. PHP 7.0 replaced with 5.5, still not working. Same code tested with a Win32 app, and it works flawlessly. Any hints?
Two years later, I casually happen to find an answer to this problem. PHP COM Objects can't perform file operations on network paths, even with all the required permissions Once the development folders were moved on the same machine where IIS runs, a bunch of previously broken tests started working. They all had one thing in common: they were using COM interfaces to save files or something like that.
Unable to query RoleEnvironment with Windows Azure SDK for PHP
I have an issue with obtaining data from Windows Azure's runtime using the newest (at the time of writing) PHP SDK from Github. Here is a test I am running on one of our hosted services: <?php include 'WindowsAzure/WindowsAzure.php'; use \WindowsAzure\ServiceRuntime\RoleEnvironment; use \WindowsAzure\ServiceRuntime\Internal\RoleEnvironmentNotAvailableException; try { echo RoleEnvironment::getDeploymentId(); } catch (RoleEnvironmentNotAvailableException $Exception) { die('Failed to find deployment id'); } RoleEnvironmentNotAvailableException is always thrown. Looking at the source, it seems to try sending commands through a named pipe (\.\pipe\WindowsAzureRuntime). Do I need to specify something within my ServiceConfiguration.csdef/cscfg in order to have access to this named pipe? Any advice would be most welcome!
Got confirmation from MS EMEA Developer Support that current SDK does not support this function. They suggested similar workaround to jonnu above - use the previous SDK's functionality for role environment / configuration settings.
The ServiceRuntime APIs run only on Cloud so if this code snippet runs on local machine it'll throw an exception as you've pointed out. Further, if you want to debug your ServiceRuntime code you've to deploy your service to WA then use remote desktop connection to access the cloud machine and debug your code.