-->

How to Download MSI installer with argument for us

2020-08-13 07:33发布

问题:

I have a .NET C# application, wrapped inside MSI installer - "myprogram.exe". I have a PHP website and a specific page where user can download the program via a link.

I would like to be able to track certain events on the .NET app. For example - "Program Opened".

It's easy to send events to my server, however how do I grab the user-id from the php server, so I can know which user did what on the .NET app?

I thought about passing an argument (the user-id) to the MSI installer but could not find a way to that.

How do I link between the PHP user-id and the .NET app?

Clarification -

Many people offered to use a login system to bind between the server and app.

This is indeed the easiest solution, however on my website I do not force the user to login to download the app (nor I request login details in the .NET app - its optional). If we dont have to ask for login details, I think we shouldn't, the experience for the user will be much better (far less steps to use the app) - greater chance for the user to download and use the Desktop app.

Consider that current flow is -> Webpage - Download click - Run - Use the app (takes 10 seconds)

With login -> Webpage - Register (confirm email?) - Redirect - Download Click - run - app Login - Use the app (takes 60-120 seconds for the user)

回答1:

Login from the program

The best way is to let the user sign-in with the same credentials in your program. This way, your program can use secure OAuth2 authentication to communicate with your back-end API. This also makes it transparent to the user that the program is communicating with the internet.

Include user-id in filename

An other way is to add the user-id to the filename of the installer during download and extract it when the installer runs. You will have to check if your installer tool allows this. Also, only do this if you user-id's are UUID's or something similar as you don't want the user to guess other id's.

App.config

A third option is to add the user-id to the App.config file. There are two ways to do this:

  1. Create your .msi with App.config uncompressed, add a user-id setting with fixed UUID. Your PHP script can lookup the UUID and replace it in the .msi binary before sending it to the user. See code snippet under MST transform
  2. Build the .msi on demand with the custom App.config. This will only work if your webserver is running on Windows or you have a remote Windows build server that can do this job.

MST transform

You could also use an MST transform and use the same binary replace trick as I've explained for point 1 under App.config.

For both options, you could use a PHP script that uses binary-safe functions to replace the values in the installer and sends the file to the user as a download:

<?php
$userId = // TODO get userId from the session or database
$data = file_get_contents("./my-installer.msi");
// I would use UUID's for template and userId, this way the size of the installer remains the same after replace
$data = str_replace("{fe06bd4e-4bed-4954-be14-42fb79a79817}", $userId, $data);
// Return the file as download
header("Cache-Control: public"); // needed for i.e.
header('Content-Disposition: attachment; filename=my-installer.msi');
header('Content-Type: application/x-msi');
header("Content-Transfer-Encoding: Binary");
echo $data;
?>

Serial number

Last method I can think of is to let the program ask for a serial number on first startup and let your website generate a unique serial number for each user.



回答2:

Please note its very likely this is not what you want to do. Never the less I'll explain a couple of ways to do it..

Using MST files with MSIs:

You can create MST files with a user-id property and generate these for each user when they download the msi and make them install the msi with a transform:

msiexec -i c:\temp\The.msi transforms=c:\temp\YourPerso.mst

See more info here: Install a transform using the command line.

MST files used a lot in large organisations, where all the MSI's have MST files with the serial numbers and etc embedded.

To make an MST file you need to download and install Microsofts Orca Tool, its part of the Microsoft Windows SDK.

Open Orca and create an MST file out of the MSI file. Basically you open the MSI file navigate to table "Property", there you see a list of parameters. Note in MSI file you'll see parameters which require default value.

Before you add/change parameters, create a new Transformation by clicking in menu on "Transform" -> "New Transform".

Afterwards you can change parameters or add new ones as you want to. When you have finished the parameter changes use function "Generate Transform" in "Transform" menu to generate a MST file.

If you then open the mst file with a HexEditor you can see the property you just added:

You could edit the file for each download by simply editing the value, eg:

You can of course (and probably should) do this the proper way using the API of the WindowsInstaller.Installer. Here is an example:

private function createTransform(mstfile, msi, config)
    writeLog InfoLog, "Generating transform " & mstfile

    dim vars: set vars = configvars(config)

    dim createPropertyTable: createPropertyTable = "create table `Property` " & _
        "(`Property` char(72) not null, `Value` longchar localizable " & _
        "primary key `Property`)"
    dim addProperty: addProperty = "insert into `Property` (`Property`, `Value`) values (?, ?)"
    dim updateProperty: updateProperty = "update `Property` set `Value` = ? where `Property` = ?"

    dim wi: set wi = createObject("WindowsInstaller.Installer")
    dim base: set base = wi.openDatabase("base.msi", msiOpenDatabaseModeCreate)
    base.openview(createPropertyTable).execute
    dim tgt: set tgt = wi.openDatabase("tgt.msi", msiOpenDatabaseModeCreate)
    tgt.openview(createPropertyTable).execute
    dim props: set props = createObject("scripting.dictionary")
    dim view: set view = msi.openView("select `Property`, `Value` from `Property`") 
    view.execute        
    dim record: set record = view.fetch
    while not record is nothing
        props(record.stringdata(1)) = true
        base.openview(addProperty).execute record
        tgt.openview(addProperty).execute record    
        set record = view.fetch
    wend

    set record = wi.createRecord(2)
    dim prop
    for each prop in properties_
        on error resume next
        dim val: val = expand(vars, prop(DepPropertyValueIdx))
        if err then
            writeLog ErrorLog, err.description
            exit function
        end if
        on error goto 0
        writeLog InfoLog, "Property " & prop(DepPropertyNameIdx) & "=" & val
        if props.exists(prop(DepPropertyNameIdx)) then
            record.stringdata(2) = prop(DepPropertyNameIdx)
            record.stringdata(1) = val
            tgt.openview(updateProperty).execute record
        else
            record.stringdata(1) = prop(DepPropertyNameIdx)
            record.stringdata(2) = val
            tgt.openview(addProperty).execute record
        end if
    next
    if not tgt.generateTransform(base, mstfile) then
        writeLog ErrorLog, "Failed to create transform"
        exit function
    end if
    tgt.createTransformSummaryInfo msi, mstfile, 0, 0
    createTransform = true
end function

Tip: To do this with managed code you're best off using the Microsoft.Deployment.WindowsInstaller.dll thats available as part of http://wix.codeplex.com/


Build an MSI for each user:

IMHO it would be far easier to do this with Nullsoft (WiX, InstallShield, INNO, etc) and build a MSI for each user. To do this you would embed a unique user id in for example an nsi script and kick off a MSI build for each download. During install the unique user id would be stored in a file, registry key or etc. I suggest you give this a go using this NSIS Wizard Editor to quickly whip up a basic NSI install script and build the MSI via a command line: makensis.

Note: While "Including the user-id in MSI filename" is easier than building an MSI for each user, users can easily change the filename. Its much, much less likely a user will audit the MSI using Orca to find an inbuilt user id.


The easiest and most logical way:

It's easy to send events to my server, however how do I grab the user-id from the php server, so I can know which user did what on the .NET app?

Do what @Jhuliano Moreno and then @WouterHuysentruit recommended:

When your application starts up for the first time simply make the user login to the program using their website credentials and record their user id in a config file, registry key or database record. Basically creating a cookie so you'll know them next time the opens the program - or make them login everytime.



回答3:

When the file is called send a parameter of the UserID, if you are using an MVC framework in your PHP you would need a new controller that gets the msi file, and renames it to name-userID.exe and then returns the file to download via the browser.