Skip to content
This repository has been archived by the owner on Jul 24, 2024. It is now read-only.

Commit

Permalink
Merge pull request #57 from 1415003719/new-api-key
Browse files Browse the repository at this point in the history
add as-api-key and mark as complete api
  • Loading branch information
Bossa573 authored Mar 20, 2023
2 parents 99c8e5f + 7807224 commit 4ba2d35
Show file tree
Hide file tree
Showing 6 changed files with 289 additions and 14 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [5.2.0] - 2023-03-17
### Added
- Add API: POST `trackings/{slug}/{tracking_number}/mark-as-completed`
- Add API: POST `trackings/{id}/mark-as-completed`
- Add Request Header: `as-api-key`
- Add AES signature
- Add RSA signature

## [5.1.3] - 2022-07-20
### Added
- Add API: POST `estimated-delivery-date/predict-batch`
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,22 @@ sudo apt-get install php5-curl
```
and restart the web server and PHP process.

Use signature headers
```php
require 'vendor/autoload.php';

$api_key = 'AFTERSHIP API KEY';
$api_secret = 'Your api secret'; // if the encryption_method = RSA, the api_secret is PEM private key
$encryption_method = 'AES or RSA';
$encryption_password = 'PEM pass phrase';

$key = ['api_key' => $api_key, 'api_secret' => $api_secret, 'encryption_method' => $encryption_method, 'encryption_password' => $encryption_password]

$couriers = new AfterShip\Couriers($key);
$trackings = new AfterShip\Trackings($key);
$last_check_point = new AfterShip\LastCheckPoint($key);
```


## Testing
1. Execute the file:
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
}
],
"require": {
"php": ">=5.4.0"
"php": ">=5.6.1",
"phpseclib/phpseclib": "~3.0"
},
"require-dev": {
"phpunit/phpunit": "~6.2",
Expand Down
69 changes: 69 additions & 0 deletions src/Encryption.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

namespace AfterShip;

use phpseclib3\Crypt\RSA;

/**
* Class Encryption
* @package AfterShip
*/
class Encryption
{
const ENCRYPTION_RSA = 'RSA';

const ENCRYPTION_AES = 'AES';

/**
* @var string
*/
private $apiSecret = '';
/**
* @var string
*/
private $encryptionMethod = '';
/**
* @var string
*/
private $encryptionPassword = '';
/**
* Request constructor.
*
* @param $apiSecret the private key
* @param $encryptionMethod 'RSA' or 'AES'
* @param $encryptionPassword the private key password
*/
function __construct($apiSecret, $encryptionMethod, $encryptionPassword)
{
$this->apiSecret = $apiSecret;
$this->encryptionMethod = $encryptionMethod;
$this->encryptionPassword = $encryptionPassword;
}

/**
* Calculate the digest of data with RSA_SIGN_PSS_2048_SHA256 algorithm.
* https://www.aftership.com/docs/tracking/quickstart/authentication#3-rsa
* @param $data The data to be encrypted.
* @return string the encrypted result with base64 encode
* @throws AfterShipException
*/
function rsaPSSSha256Encrypt($data) {
$key = RSA::loadPrivateKey($this->apiSecret, $this->encryptionPassword)
-> withHash("sha256")
-> withPadding(RSA::SIGNATURE_PSS)
-> sign($data);
return base64_encode($key);
}

/**
* Calculate the hash of data with hmac-sha256 algorithm.
* https://www.aftership.com/docs/tracking/quickstart/authentication#2-aes
* @param $data The data to be encrypted.
* @return string the encrypted result with base64 encode
* @throws AfterShipException
*/
function hmacSha256Encrypt($data) {
return base64_encode(hash_hmac('sha256', $data, $this->apiSecret, true));
}

}
169 changes: 156 additions & 13 deletions src/Request.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,44 +23,78 @@ class Request implements Requestable
/**
* @var string
*/
protected $apiKey = '';

protected $apiKey;
/**
* @var string
*/
private $apiSecret = '';
/**
* @var string
*/
private $encryptionMethod = '';
/**
* @var string
*/
private $encryptionPassword = '';
/**
* Request constructor.
*
* @param $apiKey
*/
function __construct($apiKey)
{
$this->apiKey = $apiKey;
$apiSecret = '';
$encryptionMethod = '';
$encryptionPassword= '';
$asApiKey=$apiKey;

if (is_array($apiKey)) {
if (array_key_exists('api_secret', $apiKey)) {
$apiSecret = $apiKey['api_secret'];
}
if (array_key_exists('encryption_method', $apiKey)) {
$encryptionMethod = $apiKey['encryption_method'];
}
if (array_key_exists('encryption_password', $apiKey)) {
$encryptionPassword = $apiKey['encryption_password'];
}
if (array_key_exists('api_key', $apiKey)) {
$asApiKey = $apiKey['api_key'];
}
}

$this->apiKey = $asApiKey;
$this->apiSecret = $apiSecret;
$this->encryptionMethod = $encryptionMethod;
$this->encryptionPassword = $encryptionPassword;
}

/**
* @param $url
* @param $path
* @param $method
* @param array $data
*
* @return mixed
*/
public function send($method, $url, array $data = [])
public function send($method, $path, array $data = [])
{
$url = self::API_URL . '/' . self::API_VERSION . '/' . $path;
$methodUpper = strtoupper($method);
$headers = [
'aftership-api-key' => $this->apiKey,
'content-type' => 'application/json'
];
$parameters = [
'url' => self::API_URL . '/' . self::API_VERSION . '/' . $url,
'headers' => array_map(function ($key, $value) {
return "$key: $value";
}, array_keys($headers), $headers)
'url' => $url,
];

if ($methodUpper == 'GET' && $data > 0) {
$parameters['url'] = $parameters['url'] . '?' . http_build_query($data);
} else if ($methodUpper != 'GET') {
$parameters['body'] = $this->safeJsonEncode($data);
}

$headers = $this->getHeaders($method, $parameters['url'], $data);
$parameters['headers'] = array_map(function ($key, $value) {
return "$key: $value";
}, array_keys($headers), $headers);

return $this->call($methodUpper, $parameters);
}

Expand Down Expand Up @@ -153,4 +187,113 @@ private function handleHttpStatusError($response, $curl, $code)
curl_close($curl);
throw new AfterShipException("$errType: $errCode - $errMessage", $errCode);
}

function getCanonicalizedHeaders($headers)
{
$filtered_headers = [];

foreach ($headers as $key => $value) {
// Check if the header key starts with "as-"
if (strpos($key, 'as-') === 0) {
// Convert header key to lowercase and trim leading/trailing spaces
$key = strtolower(trim($key));

// Trim leading/trailing spaces from header value
$value = trim($value);

// Concatenate header key and value
$filtered_headers[] = "{$key}:{$value}";
}
}

// Sort headers in ASCII code order
sort($filtered_headers, SORT_STRING);

// Concatenate header pairs with new line character
$header_string = implode("\n", $filtered_headers);

return $header_string;
}

function getCanonicalizedResource($url)
{
$path = "";
$query = "";
$parse_url = parse_url($url);
if (array_key_exists('path', $parse_url)) {
$path = $parse_url['path'];
}
if (array_key_exists('query', $parse_url)) {
$query = $parse_url['query'];
}
if ($query === "") {
return $path;
}

$params = explode("&", $query);
sort($params);
$queryStr = implode("&", $params);
$path .= "?" . $queryStr;

return $path;
}

function getSignString($method, $url, $data, $headers)
{
$contentMD5 = "";
$contentType = "";
if (!empty($data) && $method != "GET") {
$contentMD5 = strtoupper(md5($this->safeJsonEncode($data)));
$contentType = "application/json";
}

$canonicalizedHeaders = $this->getCanonicalizedHeaders($headers);
$canonicalizedResource = $this->getCanonicalizedResource($url);
return mb_convert_encoding($method."\n".$contentMD5."\n".$contentType."\n".$headers['date']."\n".$canonicalizedHeaders."\n".$canonicalizedResource, 'UTF-8');
}

private function getHeaders($method, $url, $data)
{
$isRSAEncryptionMethod = strcmp($this->encryptionMethod, Encryption::ENCRYPTION_RSA) == 0;
$isAESEncryptionMethod = strcmp($this->encryptionMethod, Encryption::ENCRYPTION_AES) == 0;

// if not RSA or AES encryption, just return the legacy headers
if (!$isRSAEncryptionMethod && !$isAESEncryptionMethod) {
return [
'aftership-api-key' => $this->apiKey,
'content-type' => 'application/json'
];
}

$encryption = new Encryption($this->apiSecret, $this->encryptionMethod, $this->encryptionPassword);
// get the header `date`
date_default_timezone_set('UTC');
$date = gmdate('D, d M Y H:i:s \G\M\T', time());
$contentType = "";

// get the header `content-type`
if (!empty($data) && $method != "GET") {
$contentType = "application/json";
}

$headers = [
'as-api-key' => $this->apiKey,
'date' => $date,
'content-type' => $contentType,
];
$signString = $this->getSignString($method, $url, $data, $headers);

if ($isRSAEncryptionMethod) {
$rsa = $encryption->rsaPSSSha256Encrypt($signString);
$headers['as-signature-rsa-sha256'] = $rsa;

return $headers;
}
if ($isAESEncryptionMethod) {
$rsa = $encryption->hmacSha256Encrypt($signString);
$headers['as-signature-hmac-sha256'] = $rsa;

return $headers;
}
}
}
38 changes: 38 additions & 0 deletions src/Trackings.php
Original file line number Diff line number Diff line change
Expand Up @@ -218,4 +218,42 @@ public function retrackById($trackingId)
return $this->request->send('POST', 'trackings/' . $trackingId . '/retrack');
}

/**
* Mark a tracking as completed once by slug and tracking number.
* https://www.aftership.com/docs/tracking/16935744036c1-mark-tracking-as-completed-legacy
* @param string $slug The slug of tracking provider
* @param string $trackingNumber The tracking number which is provider by tracking provider
* @param array $additionalFields The tracking additional_fields required by some courier
* @return array Response body
* @throws AfterShipException
*/
public function markAsCompleted($slug, $trackingNumber, $additionalFields = [], array $params = [])
{
if (empty($slug)) {
throw new AfterShipException("Slug cannot be empty");
}

if (empty($trackingNumber)) {
throw new AfterShipException('Tracking number cannot be empty');
}

return $this->request->send('POST', 'trackings/' . $slug . '/' . $trackingNumber . '/mark-as-completed' . TrackingAdditionalFields::buildQuery($additionalFields, '?'), $params);
}

/**
* Mark a tracking as completed once by tracking ID.
* https://www.aftership.com/docs/tracking/272f444a1eb42-mark-tracking-as-completed-by-id
* @param string $trackingId The tracking ID which is provided by AfterShip
* @return array Response body
* @throws \AfterShip\AfterShipException
*/
public function markAsCompletedById($trackingId, array $params = [])
{
if (empty($trackingId)) {
throw new AfterShipException('Tracking ID cannot be empty');
}

return $this->request->send('POST', 'trackings/' . $trackingId . '/mark-as-completed', $params);
}

}

0 comments on commit 4ba2d35

Please sign in to comment.