Skip to content

Commit

Permalink
Add --setup-api and --setup-webhook commands to bin/mwrun and update …
Browse files Browse the repository at this point in the history
…README.
  • Loading branch information
colinmollenhour committed Sep 13, 2024
1 parent 3c9dff1 commit 86d0fdb
Show file tree
Hide file tree
Showing 3 changed files with 249 additions and 22 deletions.
70 changes: 53 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,12 +113,17 @@ Installation
$ chmod go+rwX tmp logs
$ bin/update
```
2. Copy and edit the sample config file to add your configuration:

2. Setup the API connection the ShipStream instance:
```
$ cp app/etc/local.sample.xml app/etc/local.xml
$ chmod 666 app/etc/local.xml
$ bin/mwrun --setup-api
...
API credentials saved to /var/www/html/app/etc/local.xml
```
Example:

Your `app/etc/local.xml` file should look something like this:
```xml
<?xml version="1.0"?>
<config>
Expand All @@ -131,29 +136,40 @@ Installation
<timezone>America/New_York</timezone>
</system>
<api>
<base_url>https://example.shipstream.app/api/jsonrpc</base_url>
<login>{api_username}</login>
<password>{api_password}</password>
<secret_key>{secret_key}</secret_key>
<base_url>https://example.shipstream.app/api/jsonrpc/</base_url>
<login>{your_api_username}</login>
<password>{your_api_password}</password>
<secret_key></secret_key>
</api>
</middleware>
</default>
</config>
```

3. Clone the `ShipStream_Test` plugin and run the `update_ip` method to confirm a successful setup!
3. Clone the `ShipStream_Test` plugin:
```
$ bin/modman init
$ bin/modman clone https://github.com/shipstream/plugin-test.git
$ bin/mwrun --list-plugins
Plugin Code Plugin Name
----------- -----------
ShipStream_Test Test Plugin
$ bin/mwrun ShipStream_Test --list-actions
update_ip
* Fetches your public IP, saves it in the remote storage, fetches it and logs it.
...
```
The source code for the `ShipStream_Test` plugin will be located at `.modman/plugin-test` because the `bin/modman clone ...`
command effectively performs `(cd .modman; git clone ...); bin/modman deploy ...`. The `modman` utility creates the symlinks
as defined in the module's `modman` file so your repository files can all stay in one place for easy git workflows.

4. Run the `update_ip` action for the `ShipStream_Test` plugin to confirm a successful connection:
```
$ bin/mwrun ShipStream_Test update_ip
Agent 007's IP is x.x.x.x, last updated at 2020-01-19T14:41:23+00:00.
```

The source code for the `ShipStream_Test` plugin will be located at `.modman/plugin-test` because the `bin/modman clone ...`
command effectively performs `(cd .modman; git clone ...); bin/modman deploy ...`. The `modman` utility creates the symlinks
as defined in the module's `modman` file so your repository files can all stay in one place for easy git workflows.

If you completed all of the steps above successfully, your environment is working properly and you can now start developing your plugin!

### Advanced

Expand All @@ -164,10 +180,13 @@ You can use a `.env` file in the root of the project to set some configuration o

### HTTPS Tunnel

If you need to support callbacks and webhooks from systems not in your local development environment you need your
If you need to support [events](#shipstream-events), [callbacks](#third-party-remote-callbacks) or [webhooks](#third-party-webhooks)
(all use http requests from systems not in your local development environment), you need your middleware
url to be publicly accessible. One easy and free way to accomplish this is to use [ngrok](https://ngrok.com) or [localhost.run](https://localhost.run)
which are simple tunneling services. The ngrok service uses its own command line interface and is more robust, while
localhost.run only requires the common `ssh` command.
which are simple tunneling services which can expose your local development environment to a publicly-accessible https url.

The ngrok service uses its own command line interface and is more robust, while localhost.run only requires the common `ssh` command.
***Tip:*** Create an account with ngrok to get one free persistent subdomain which makes it easier to configure webhooks.

```
$ ngrok http 80
Expand Down Expand Up @@ -213,8 +232,25 @@ plans this will only last a few hours before it needs to be refreshed with a new
</config>
```

***NOTE:***
If you update your `base_url`, you may need to also re-register that url for any plugins that make use of callbacks.
***Note:***
Any time your public url changes (e.g. your proxy disconnects and you reconnect with a new randomly generated url),
you will need to register or re-register a webhook for any plugins that you are developing so that you can receive the real-time events.

1. Update `app/etc/local.xml` with the new `base_url` value as described above.
2. With your https tunnel running, register a webhook for the plugin so you can receive events:
```
$ bin/mwrun ShipStream_Test --setup-webhook
Secret Key saved to /var/www/html/app/etc/local.xml
Webhook created for https://....
```

3. Create an order for the same merchant as your API key, wait a few seconds for the webhook to process, and check the logs for the event:
```
$ tail logs/webhooks.log
2024-09-13T16:10:58+00:00 Received webhook for order:created topic with message: {"merchant_id":"9","merchant_code":"acme_inc","topic":"order:created","
$ tail logs/main.log
2024-09-13T16:10:58+00:00 Order # 1100000116 was created.
```

Developer Guide
===============
Expand Down
10 changes: 7 additions & 3 deletions app/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -259,10 +259,11 @@ public function loadPluginClass($suffix)
* Retrieve configuration value
*
* @param string $path
* @return null|string
* @param bool $asString
* @return null|string|Varien_Simplexml_Element
* @throws Exception
*/
public function getConfig($path)
public function getConfig($path, $asString = TRUE)
{
if (empty($path)) {
return NULL;
Expand Down Expand Up @@ -295,7 +296,10 @@ public function getConfig($path)
if ($result === FALSE) {
return NULL;
}
return $result->__toString();
if ($asString) {
return $result->__toString();
}
return $result;
}

/**
Expand Down
191 changes: 189 additions & 2 deletions run.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@
$usage = "
Usage:
bin/mwrun --list-plugins
bin/mwrun {command}
--list-plugins List all plugins that are detected
--setup-api Setup API credentials
bin/mwrun <plugin> <method>|--info|--list-actions|--list-plugins|--listen [--debug]
bin/mwrun <plugin> <method>|{command} [--debug]
--list-actions List all methods that can be invoked from the command line
--listen Connect to Redis server for receiving ShipStream events
--respond-url Show url for receiving ShipStream events
--webhook-url Show url for receiving third-party webhooks
--callback-url <name> Show url for receiving callback requests
--diagnostics Show connection diagnostics
--setup-webhook Setup webhook for plugin
";

error_reporting(E_ALL | E_STRICT);
Expand Down Expand Up @@ -54,6 +56,69 @@
}
}
exit;
} else if ($argv[1] === '--setup-api') {
$configFile = __DIR__ . '/app/etc/local.xml';
if ( ! file_exists($configFile)) {
echo "Config file $configFile not found.\n";
exit (1);
}
if ( ! is_writable($configFile)) {
echo "Config file $configFile is not writable. Example: `sudo chmod 666 app/etc/local.xml`\n";
exit (1);
}
echo "What is your ShipStream instance base url?\n";
$baseUrl = rtrim(trim(fgets(STDIN)), '/');
$baseUrl = preg_replace('#/api/jsonrpc/?$#', '', $baseUrl);
// Expected response: '{"error":{"code":-32600,"message":"Invalid Request","data":"No method specified."},"id":null}'
try {
$client = new \GuzzleHttp\Client(['base_uri' => $baseUrl]);
$response = $client->post('api/jsonrpc/');
$data = json_decode($response->getBody()->getContents(), FALSE, 512, JSON_THROW_ON_ERROR);
if (!isset($data->error->code) || $data->error->code !== -32600) {
throw new Exception('Invalid response from ShipStream. Check that your base url is correct.');
}
} catch (Exception $e) {
echo "Failed to validate ShipStream base url: {$e->getMessage()}\n";
exit (1);
}
echo "Please go to $baseUrl/admin/api_user/new/ to create a new Merchant API User if needed.\n";
echo "What is your ShipStream API username?\n";
$username = trim(fgets(STDIN));
echo "What is your ShipStream API password?\n";
$password = trim(fgets(STDIN));
try {
$response = $client->post('api/jsonrpc/', [
'json' => [
'jsonrpc' => '2.0',
'method' => 'login',
'params' => [$username, $password],
'id' => 1,
],
]);
$data = json_decode($response->getBody()->getContents(), TRUE, 512, JSON_THROW_ON_ERROR);
if (!empty($data['error']['message'])) {
throw new Exception('Failed to login to ShipStream: ' . $data['error']['message']);
}
if (!isset($data['result']) || !$data['result']) {
throw new Exception('Invalid response from ShipStream. Check that your username and password are correct.');
}
} catch (Exception $e) {
echo "Failed to login to ShipStream: {$e->getMessage()}\n";
exit (1);
}
$xml = simplexml_load_file($configFile);
$xml->default->middleware->api = $xml->default->middleware->api ?: new SimpleXMLElement('<api/>');
$xml->default->middleware->api->base_url = "$baseUrl/api/jsonrpc/";
$xml->default->middleware->api->login = $username;
$xml->default->middleware->api->password = $password;
if (!$xml->saveXML($configFile)) {
echo "Could not write config file. Please check permissions.\n";
echo "Intended contents of $configFile:\n";
echo $xml->asXML();
exit (1);
}
echo "API credentials saved to $configFile\n";
exit;
}
if ($argc <= 2) {
die($usage);
Expand Down Expand Up @@ -121,6 +186,128 @@
echo "Running crontab for {$argv[3]}...\n";
echo "NOT YET IMPLEMENTED!! Please run your cron tasks using the method name directly.\n";
// TODO
} else if ($method === '--setup-webhook') {
$configFile = __DIR__ . '/app/etc/local.xml';
if ( ! file_exists($configFile)) {
echo "Config file $configFile not found.\n";
exit (1);
}
if ( ! is_writable($configFile)) {
echo "Config file $configFile is not writable. Example: `sudo chmod 666 app/etc/local.xml`\n";
exit (1);
}
// Generate a secret key and create a webhook for the methods required by the plugin
$topics = [];
$eventsNode = $middleware->getConfig('plugin/' . $plugin . '/events', FALSE);
if ($eventsNode) {
foreach ($eventsNode->children() as $entityType => $event) {
foreach ($event->children() as $eventName => $enabled) {
if ($enabled) {
$topics[] = "$entityType:$eventName";
}
}
}
}
if (empty($topics)) {
echo "No events are enabled for this plugin.\n";
exit (1);
}
if ( ! $middleware->getConfig('middleware/api/base_url')) {
echo "Connection is not configured. Please run bin/mwrun --setup-api\n";
exit (1);
}
$secretKey = $middleware->getConfig('middleware/api/secret_key');
if ( ! $secretKey) {
$secretKey = bin2hex(random_bytes(16));
$xml = simplexml_load_file($configFile);
$xml->default->middleware->api->secret_key = $secretKey;
if (file_put_contents($configFile, $xml->asXML())) {
echo "Secret key saved to $configFile\n";
} else {
echo "Could not write config file. Please check permissions.\n";
echo "Intended contents of $configFile:\n";
echo $xml->asXML();
exit (1);
}
}
$localUrl = trim($middleware->getConfig('middleware/system/base_url'));
try {
$client = new \GuzzleHttp\Client(['base_uri' => $localUrl]);
$client->get('hello.php');
} catch (Exception $e) {
echo "Failed to connect to middleware environment using public url $localUrl\nError: {$e->getMessage()}\n";
echo "Make sure your HTTPS tunnel is running and the public url is correct in app/etc/local.xml at default/middleware/system/base_url.\n";
exit (1);
}

$webhookUrl = $middleware->getRespondUrl();

$client = new \GuzzleHttp\Client([
'base_uri' => $middleware->getConfig('middleware/api/base_url'),
'auth' => [$middleware->getConfig('middleware/api/login'), $middleware->getConfig('middleware/api/password')],
]);

$response = $client->post('', [
'json' => [
'jsonrpc' => '2.0',
'method' => 'call',
'params' => [
null,
'webhook.list',
[],
],
'id' => 1,
],
]);
$data = json_decode($response->getBody()->getContents(), TRUE, 512, JSON_THROW_ON_ERROR);
if (!empty($data['error']['message'])) {
throw new Exception('Failed to create webhook: ' . $data['error']['message']);
}
foreach ($data['result'] as $webhook) {
if ($webhook['url'] === $webhookUrl || $webhook['extra_headers'] === "X-Middleware-Id: $secretKey") {
$client->post('', [
'json' => [
'jsonrpc' => '2.0',
'method' => 'call',
'params' => [
null,
'webhook.delete',
[
$webhook['webhook_id'],
],
],
'id' => 2,
],
]);
echo "Deleted existing webhook for {$webhook['url']}\n";
}
}
$response = $client->post('', [
'json' => [
'jsonrpc' => '2.0',
'method' => 'call',
'params' => [
null,
'webhook.create',
[
[
'is_active' => true,
'url' => $webhookUrl,
'topics' => $topics,
'secret_key' => $secretKey,
'extra_headers' => "X-Middleware-Id: $secretKey",
]
],
],
'id' => 2,
],
]);
$data = json_decode($response->getBody()->getContents(), TRUE, 512, JSON_THROW_ON_ERROR);
if (!empty($data['error']['message'])) {
throw new Exception('Failed to create webhook: ' . $data['error']['message']);
}

echo "Webhook created for $webhookUrl\n";
} else {
$middleware->run($method);
}
Expand Down

0 comments on commit 86d0fdb

Please sign in to comment.