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

Commit 4ba2d35

Browse files
authored
Merge pull request #57 from 1415003719/new-api-key
add as-api-key and mark as complete api
2 parents 99c8e5f + 7807224 commit 4ba2d35

File tree

6 files changed

+289
-14
lines changed

6 files changed

+289
-14
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [5.2.0] - 2023-03-17
8+
### Added
9+
- Add API: POST `trackings/{slug}/{tracking_number}/mark-as-completed`
10+
- Add API: POST `trackings/{id}/mark-as-completed`
11+
- Add Request Header: `as-api-key`
12+
- Add AES signature
13+
- Add RSA signature
14+
715
## [5.1.3] - 2022-07-20
816
### Added
917
- Add API: POST `estimated-delivery-date/predict-batch`

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,22 @@ sudo apt-get install php5-curl
6868
```
6969
and restart the web server and PHP process.
7070

71+
Use signature headers
72+
```php
73+
require 'vendor/autoload.php';
74+
75+
$api_key = 'AFTERSHIP API KEY';
76+
$api_secret = 'Your api secret'; // if the encryption_method = RSA, the api_secret is PEM private key
77+
$encryption_method = 'AES or RSA';
78+
$encryption_password = 'PEM pass phrase';
79+
80+
$key = ['api_key' => $api_key, 'api_secret' => $api_secret, 'encryption_method' => $encryption_method, 'encryption_password' => $encryption_password]
81+
82+
$couriers = new AfterShip\Couriers($key);
83+
$trackings = new AfterShip\Trackings($key);
84+
$last_check_point = new AfterShip\LastCheckPoint($key);
85+
```
86+
7187

7288
## Testing
7389
1. Execute the file:

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
}
1212
],
1313
"require": {
14-
"php": ">=5.4.0"
14+
"php": ">=5.6.1",
15+
"phpseclib/phpseclib": "~3.0"
1516
},
1617
"require-dev": {
1718
"phpunit/phpunit": "~6.2",

src/Encryption.php

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
namespace AfterShip;
4+
5+
use phpseclib3\Crypt\RSA;
6+
7+
/**
8+
* Class Encryption
9+
* @package AfterShip
10+
*/
11+
class Encryption
12+
{
13+
const ENCRYPTION_RSA = 'RSA';
14+
15+
const ENCRYPTION_AES = 'AES';
16+
17+
/**
18+
* @var string
19+
*/
20+
private $apiSecret = '';
21+
/**
22+
* @var string
23+
*/
24+
private $encryptionMethod = '';
25+
/**
26+
* @var string
27+
*/
28+
private $encryptionPassword = '';
29+
/**
30+
* Request constructor.
31+
*
32+
* @param $apiSecret the private key
33+
* @param $encryptionMethod 'RSA' or 'AES'
34+
* @param $encryptionPassword the private key password
35+
*/
36+
function __construct($apiSecret, $encryptionMethod, $encryptionPassword)
37+
{
38+
$this->apiSecret = $apiSecret;
39+
$this->encryptionMethod = $encryptionMethod;
40+
$this->encryptionPassword = $encryptionPassword;
41+
}
42+
43+
/**
44+
* Calculate the digest of data with RSA_SIGN_PSS_2048_SHA256 algorithm.
45+
* https://www.aftership.com/docs/tracking/quickstart/authentication#3-rsa
46+
* @param $data The data to be encrypted.
47+
* @return string the encrypted result with base64 encode
48+
* @throws AfterShipException
49+
*/
50+
function rsaPSSSha256Encrypt($data) {
51+
$key = RSA::loadPrivateKey($this->apiSecret, $this->encryptionPassword)
52+
-> withHash("sha256")
53+
-> withPadding(RSA::SIGNATURE_PSS)
54+
-> sign($data);
55+
return base64_encode($key);
56+
}
57+
58+
/**
59+
* Calculate the hash of data with hmac-sha256 algorithm.
60+
* https://www.aftership.com/docs/tracking/quickstart/authentication#2-aes
61+
* @param $data The data to be encrypted.
62+
* @return string the encrypted result with base64 encode
63+
* @throws AfterShipException
64+
*/
65+
function hmacSha256Encrypt($data) {
66+
return base64_encode(hash_hmac('sha256', $data, $this->apiSecret, true));
67+
}
68+
69+
}

src/Request.php

Lines changed: 156 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,44 +23,78 @@ class Request implements Requestable
2323
/**
2424
* @var string
2525
*/
26-
protected $apiKey = '';
27-
26+
protected $apiKey;
27+
/**
28+
* @var string
29+
*/
30+
private $apiSecret = '';
31+
/**
32+
* @var string
33+
*/
34+
private $encryptionMethod = '';
35+
/**
36+
* @var string
37+
*/
38+
private $encryptionPassword = '';
2839
/**
2940
* Request constructor.
3041
*
3142
* @param $apiKey
3243
*/
3344
function __construct($apiKey)
3445
{
35-
$this->apiKey = $apiKey;
46+
$apiSecret = '';
47+
$encryptionMethod = '';
48+
$encryptionPassword= '';
49+
$asApiKey=$apiKey;
50+
51+
if (is_array($apiKey)) {
52+
if (array_key_exists('api_secret', $apiKey)) {
53+
$apiSecret = $apiKey['api_secret'];
54+
}
55+
if (array_key_exists('encryption_method', $apiKey)) {
56+
$encryptionMethod = $apiKey['encryption_method'];
57+
}
58+
if (array_key_exists('encryption_password', $apiKey)) {
59+
$encryptionPassword = $apiKey['encryption_password'];
60+
}
61+
if (array_key_exists('api_key', $apiKey)) {
62+
$asApiKey = $apiKey['api_key'];
63+
}
64+
}
65+
66+
$this->apiKey = $asApiKey;
67+
$this->apiSecret = $apiSecret;
68+
$this->encryptionMethod = $encryptionMethod;
69+
$this->encryptionPassword = $encryptionPassword;
3670
}
3771

3872
/**
39-
* @param $url
73+
* @param $path
4074
* @param $method
4175
* @param array $data
4276
*
4377
* @return mixed
4478
*/
45-
public function send($method, $url, array $data = [])
79+
public function send($method, $path, array $data = [])
4680
{
81+
$url = self::API_URL . '/' . self::API_VERSION . '/' . $path;
4782
$methodUpper = strtoupper($method);
48-
$headers = [
49-
'aftership-api-key' => $this->apiKey,
50-
'content-type' => 'application/json'
51-
];
5283
$parameters = [
53-
'url' => self::API_URL . '/' . self::API_VERSION . '/' . $url,
54-
'headers' => array_map(function ($key, $value) {
55-
return "$key: $value";
56-
}, array_keys($headers), $headers)
84+
'url' => $url,
5785
];
86+
5887
if ($methodUpper == 'GET' && $data > 0) {
5988
$parameters['url'] = $parameters['url'] . '?' . http_build_query($data);
6089
} else if ($methodUpper != 'GET') {
6190
$parameters['body'] = $this->safeJsonEncode($data);
6291
}
6392

93+
$headers = $this->getHeaders($method, $parameters['url'], $data);
94+
$parameters['headers'] = array_map(function ($key, $value) {
95+
return "$key: $value";
96+
}, array_keys($headers), $headers);
97+
6498
return $this->call($methodUpper, $parameters);
6599
}
66100

@@ -153,4 +187,113 @@ private function handleHttpStatusError($response, $curl, $code)
153187
curl_close($curl);
154188
throw new AfterShipException("$errType: $errCode - $errMessage", $errCode);
155189
}
190+
191+
function getCanonicalizedHeaders($headers)
192+
{
193+
$filtered_headers = [];
194+
195+
foreach ($headers as $key => $value) {
196+
// Check if the header key starts with "as-"
197+
if (strpos($key, 'as-') === 0) {
198+
// Convert header key to lowercase and trim leading/trailing spaces
199+
$key = strtolower(trim($key));
200+
201+
// Trim leading/trailing spaces from header value
202+
$value = trim($value);
203+
204+
// Concatenate header key and value
205+
$filtered_headers[] = "{$key}:{$value}";
206+
}
207+
}
208+
209+
// Sort headers in ASCII code order
210+
sort($filtered_headers, SORT_STRING);
211+
212+
// Concatenate header pairs with new line character
213+
$header_string = implode("\n", $filtered_headers);
214+
215+
return $header_string;
216+
}
217+
218+
function getCanonicalizedResource($url)
219+
{
220+
$path = "";
221+
$query = "";
222+
$parse_url = parse_url($url);
223+
if (array_key_exists('path', $parse_url)) {
224+
$path = $parse_url['path'];
225+
}
226+
if (array_key_exists('query', $parse_url)) {
227+
$query = $parse_url['query'];
228+
}
229+
if ($query === "") {
230+
return $path;
231+
}
232+
233+
$params = explode("&", $query);
234+
sort($params);
235+
$queryStr = implode("&", $params);
236+
$path .= "?" . $queryStr;
237+
238+
return $path;
239+
}
240+
241+
function getSignString($method, $url, $data, $headers)
242+
{
243+
$contentMD5 = "";
244+
$contentType = "";
245+
if (!empty($data) && $method != "GET") {
246+
$contentMD5 = strtoupper(md5($this->safeJsonEncode($data)));
247+
$contentType = "application/json";
248+
}
249+
250+
$canonicalizedHeaders = $this->getCanonicalizedHeaders($headers);
251+
$canonicalizedResource = $this->getCanonicalizedResource($url);
252+
return mb_convert_encoding($method."\n".$contentMD5."\n".$contentType."\n".$headers['date']."\n".$canonicalizedHeaders."\n".$canonicalizedResource, 'UTF-8');
253+
}
254+
255+
private function getHeaders($method, $url, $data)
256+
{
257+
$isRSAEncryptionMethod = strcmp($this->encryptionMethod, Encryption::ENCRYPTION_RSA) == 0;
258+
$isAESEncryptionMethod = strcmp($this->encryptionMethod, Encryption::ENCRYPTION_AES) == 0;
259+
260+
// if not RSA or AES encryption, just return the legacy headers
261+
if (!$isRSAEncryptionMethod && !$isAESEncryptionMethod) {
262+
return [
263+
'aftership-api-key' => $this->apiKey,
264+
'content-type' => 'application/json'
265+
];
266+
}
267+
268+
$encryption = new Encryption($this->apiSecret, $this->encryptionMethod, $this->encryptionPassword);
269+
// get the header `date`
270+
date_default_timezone_set('UTC');
271+
$date = gmdate('D, d M Y H:i:s \G\M\T', time());
272+
$contentType = "";
273+
274+
// get the header `content-type`
275+
if (!empty($data) && $method != "GET") {
276+
$contentType = "application/json";
277+
}
278+
279+
$headers = [
280+
'as-api-key' => $this->apiKey,
281+
'date' => $date,
282+
'content-type' => $contentType,
283+
];
284+
$signString = $this->getSignString($method, $url, $data, $headers);
285+
286+
if ($isRSAEncryptionMethod) {
287+
$rsa = $encryption->rsaPSSSha256Encrypt($signString);
288+
$headers['as-signature-rsa-sha256'] = $rsa;
289+
290+
return $headers;
291+
}
292+
if ($isAESEncryptionMethod) {
293+
$rsa = $encryption->hmacSha256Encrypt($signString);
294+
$headers['as-signature-hmac-sha256'] = $rsa;
295+
296+
return $headers;
297+
}
298+
}
156299
}

src/Trackings.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,4 +218,42 @@ public function retrackById($trackingId)
218218
return $this->request->send('POST', 'trackings/' . $trackingId . '/retrack');
219219
}
220220

221+
/**
222+
* Mark a tracking as completed once by slug and tracking number.
223+
* https://www.aftership.com/docs/tracking/16935744036c1-mark-tracking-as-completed-legacy
224+
* @param string $slug The slug of tracking provider
225+
* @param string $trackingNumber The tracking number which is provider by tracking provider
226+
* @param array $additionalFields The tracking additional_fields required by some courier
227+
* @return array Response body
228+
* @throws AfterShipException
229+
*/
230+
public function markAsCompleted($slug, $trackingNumber, $additionalFields = [], array $params = [])
231+
{
232+
if (empty($slug)) {
233+
throw new AfterShipException("Slug cannot be empty");
234+
}
235+
236+
if (empty($trackingNumber)) {
237+
throw new AfterShipException('Tracking number cannot be empty');
238+
}
239+
240+
return $this->request->send('POST', 'trackings/' . $slug . '/' . $trackingNumber . '/mark-as-completed' . TrackingAdditionalFields::buildQuery($additionalFields, '?'), $params);
241+
}
242+
243+
/**
244+
* Mark a tracking as completed once by tracking ID.
245+
* https://www.aftership.com/docs/tracking/272f444a1eb42-mark-tracking-as-completed-by-id
246+
* @param string $trackingId The tracking ID which is provided by AfterShip
247+
* @return array Response body
248+
* @throws \AfterShip\AfterShipException
249+
*/
250+
public function markAsCompletedById($trackingId, array $params = [])
251+
{
252+
if (empty($trackingId)) {
253+
throw new AfterShipException('Tracking ID cannot be empty');
254+
}
255+
256+
return $this->request->send('POST', 'trackings/' . $trackingId . '/mark-as-completed', $params);
257+
}
258+
221259
}

0 commit comments

Comments
 (0)