Title: Clientexec 7.2.2 — How can a Snapin securely execute customer-initiated server plugin actions?

HostOncilla

New Member
Hi everyone,


I’m developing a custom Plesk Manager Snapin for Clientexec 7.2.2, using the existing Plesk10 server plugin.


The aim is to extend the customer’s hosting package with additional management features, including:


  • DNS Management (view, add, edit and delete records)
  • Email account management
  • Other Plesk hosting management features in the future

The Snapin is already integrated into the customer’s hosting package, and the DNS Management interface is working. It can retrieve and display the customer’s existing Plesk DNS records.


The problem


I’m struggling to establish the correct way for a customer-facing Snapin to execute backend actions securely.


Customer GET requests to registered Snapin views work correctly, but attempts to execute POST requests or invoke Snapin actions have failed.


Here is what we’ve established:


  1. addMappingView() works and successfully renders custom customer-facing pages.
  2. A POST request to a working mapped view returned HTTP 200 with an empty response body, whereas GET rendered the page correctly.
  3. Attempts to invoke actions through fuse=admin&action=doplugin&controller=plugin&type=snapin resulted in redirects rather than executing the intended handler.
  4. The existing Plesk10 server plugin doesn’t appear to declare Registered Actions For Customer.
  5. Other server plugins, such as SolusVM, use Registered Actions For Customer and methods such as doauthenticateClient($args).
  6. Some Clientexec core controllers are ionCube-encoded, making it difficult to trace the request-dispatch process.

What I need help understanding


1. Customer-facing Snapin actions


What is the officially supported way for an authenticated customer to submit a POST request to a Snapin backend handler in Clientexec 7.2.2?


  • What URL, controller and action should be used?
  • Can addMappingAction() handle customer POST requests?
  • How should the action be registered and invoked?
  • What parameters are required?
  • What response format is expected (HTML, JSON, redirect, etc.)?
  • Is there a working example of a customer-facing Snapin performing an AJAX POST request?

2. Server plugin customer actions


How does Clientexec invoke methods declared through Registered Actions For Customer?


For example, if a server plugin declares authenticateClient and implements doauthenticateClient($args), how is that method invoked from a customer’s product page?


I’d particularly appreciate a working example showing the correct request URL, HTTP method, parameters and response handling.


3. Authentication, authorization and CSRF protection


How should we securely verify:


  • The customer is logged in.
  • The hosting package belongs to that customer.
  • The package uses the correct Plesk10 server plugin.
  • The customer is authorized to perform the requested operation.
  • The POST request passes Clientexec’s CSRF protection.

Does Clientexec perform these checks automatically for customer server-plugin actions, or must the Snapin implement some of them?


4. Accessing the hosting package and server configuration


From a customer-facing Snapin action, what is the supported way to obtain the relevant hosting package object, package ID, associated server configuration and domain?


Can the Snapin safely call methods on the existing Plesk10 plugin, or should it use another supported Clientexec API?


5. Recommended architecture


For a Snapin that adds DNS and email management to an existing Plesk10 hosting package, which approach is recommended?


  • Implement backend actions directly inside the Snapin.
  • Register additional customer actions in the Plesk10 server plugin.
  • Use another supported Clientexec extension mechanism.

I’d prefer not to modify the production Plesk10 plugin unless necessary, since it already handles provisioning and other essential hosting operations.


6. Working code example


If possible, could someone provide a minimal working example for Clientexec 7.2.2 showing:


  • A button on a customer’s hosting package page.
  • An authenticated POST request.
  • A backend PHP handler.
  • Verification that the customer owns the hosting package.
  • A simple JSON response such as {"success":true,"message":"Action executed"}.

The example doesn’t need to make any changes to Plesk. I just need to establish the correct, secure customer-to-backend communication mechanism.


What already works


The customer-facing interface, Plesk DNS record retrieval, and registered GET views are working. DNS modifications are intentionally disabled until the backend action mechanism and its security requirements are confirmed.


Any documentation, working code examples, or guidance from developers familiar with Clientexec 7.2.x would be greatly appreciated.


Thanks in advance!
 
Hi HostOncilla,

I ran into nearly all of this building our Client Forum for Clientexec, which is a snapin where logged-in clients create threads, reply and edit posts. Here's what worked for me. I verified it on a 7.3.0 beta dev install, but I'd expect 7.2.2 to behave the same. Check it on your version.

Your empty 200 on POST

That's expected. Clientexec drops POSTs sent to a snapin view URL (fuse=admin&view=snapin&controller=snapins&plugin=…&h=…), and your view method never runs. GET to the view is fine, but POSTs need a different route.

1. Customer-facing Snapin actions

POST to the plugin action route:

index.php?fuse=admin&controller=plugin&action=doplugin&type=snapin&plugin=YOURSNAPIN&pluginaction=YOURACTION

Clientexec passes that request to your snapin's callAction() method, and it works for logged-in clients, not just staff, despite the fuse=admin part. All of our forum's client posts go through it.

The catch, and probably why you got redirects: Clientexec checks a sessionHash field on POSTs. Put the value of CE_Lib::getSessionHash() in your form as sessionHash. Clientexec may remove that field from $_POST after checking it, so send the same value a second time under your own field name for your own check.

You can't register extra actions like addMappingAction(). Instead, callAction() reads $_REQUEST['pluginaction'] and you branch on it. For the response, echo JSON and return. For normal form posts, send a Location redirect (303) back to your GET view, and call session_write_close() first so any message you stored in the session survives. Clientexec doesn't display its own flash messages on snapin pages, so keep your own in the session and show them on the next view.

For AJAX, send the request form-encoded (FormData), not as a JSON body, so the sessionHash shows up in $_POST.

2. Registered Actions For Customer

Those show up as buttons on the product page and run the server plugin's matching do… method with the arguments Clientexec builds from the package. They're fine for one-click things like SolusVM's panel login, but you can't send your own form fields through them. Plesk10 doesn't register any, and I wouldn't add any to a production plugin. For DNS editing, use the snapin route above.

3. Authentication, authorization and CSRF

Clientexec does the sessionHash check and gives you $this->user. Everything else is up to you. In callAction(), check these in order:

  1. Logged in: if !is_object($this->user) or $this->user->isAnonymous(), fail.
  2. CSRF: check hash_equals(CE_Lib::getSessionHash(), $_POST['your_token']).
  3. Allowed action: check the pluginaction against a whitelist.
  4. Owns the package: load the package from the posted ID, and compare its customer ID with $this->user->getId().
  5. Right package: make sure it's on a Plesk10 server and active.
  6. Allowed record: check that the DNS record ID being changed belongs to that package's domain, and validate every field server-side.

4. Package and server configuration

Load everything server-side from the package ID after the ownership check: the domain, the server it's on, and that server's hostname and credentials. Never send credentials to the browser. I'd call the Plesk API from a small class inside your snapin using those server details, rather than calling into or editing the Plesk10 plugin. Since part of Clientexec is ionCube-encoded, dump the package and server objects once in a test view to confirm the accessor names on 7.2.2.

5. Recommended architecture

Keep it all in the snapin:

  • GET views for the pages
  • callAction() for every POST, behind one whitelist
  • A separate helper class for the Plesk calls

Keep the helper code out of the plugin class itself. The parent class is encoded, so any extra method you add there could clash with one you can't see. In ours, the plugin class only has the methods Clientexec calls, and everything else lives in a lib folder. Your Plesk10 provisioning stays untouched.

6. Minimal example

The JavaScript on your customer view:

JavaScript:
// Your form needs two hidden fields, both set to CE_Lib::getSessionHash():
//   name="sessionHash"  (checked by Clientexec)
//   name="pm_token"     (checked by your handler)
// plus a hidden package_id and your record fields.

document.getElementById('dnsAdd').addEventListener('submit', function (e) {
  e.preventDefault();
  fetch('index.php?fuse=admin&controller=plugin&action=doplugin&type=snapin&plugin=pleskmanager&pluginaction=dnsadd',
    { method: 'POST', body: new FormData(this), credentials: 'same-origin' })
    .then(function (r) { return r.json(); })
    .then(function (d) { alert(d.message); });
});

In your snapin class:

PHP:
public function callAction($callback = true)
{
    header('Content-Type: application/json');
    $action = isset($_REQUEST['pluginaction']) ? (string) $_REQUEST['pluginaction'] : '';
    echo json_encode((new PleskManagerActions($this->db, $this->user))->handle($action));
}

And the helper class:

PHP:
public function handle($action)
{
    if (!is_object($this->user) || $this->user->isAnonymous()) {
        return ['success' => false, 'message' => 'Please log in.'];
    }
    $token = isset($_POST['pm_token']) ? (string) $_POST['pm_token'] : '';
    if (!hash_equals((string) CE_Lib::getSessionHash(), $token)) {
        return ['success' => false, 'message' => 'Your session expired. Reload the page.'];
    }
    if (!in_array($action, ['dnsadd', 'dnsedit', 'dnsdelete'], true)) {
        return ['success' => false, 'message' => 'Unknown action.'];
    }
    // Load the package from $_POST['package_id'], confirm its customer ID equals
    // $this->user->getId(), it's on a Plesk10 server and it's active.
    // Validate the record fields, then call Plesk.
    return ['success' => true, 'message' => 'Action executed'];
}

Hope that saves you some time. Happy to compare notes if you get stuck.

Rob, K & K Web Services
 
Back
Top