[Bps-public-commit] rt-extension-assets-applegsx branch, uat, created. 1.0-11-gf1abf55

Michel Rodriguez michel at bestpractical.com
Wed Aug 28 16:45:07 EDT 2019


The branch, uat has been created
        at  f1abf551e6349562bfe923f7b3fdc112a0cc4da4 (commit)

- Log -----------------------------------------------------------------
commit f1abf551e6349562bfe923f7b3fdc112a0cc4da4
Author: michel <michel at bestpractical.com>
Date:   Wed Aug 21 19:35:17 2019 +0200

    The module now uses the new JSON based Apple GSX API

diff --git a/README b/README
index 706ea38..9cada11 100644
--- a/README
+++ b/README
@@ -38,6 +38,21 @@ CONFIGURATION
     account number, you must then get certificate and key files from Apple
     and your server IP addresses must be whitelisted by Apple.
 
+    The configuration for the service uses the following variables:
+
+        # test server
+        Set( $AppleGSXApiBase,  'https://partner-connect-uat.apple.com/gsx/api');
+        Set( $AppleGSXGetToken, 'https://gsx2-uat.apple.com/gsx/api/login');
+
+    or
+
+        # production server
+        Set( $AppleGSXApiBase,  'https://partner-connect.apple.com/gsx/api');
+        Set( $AppleGSXGetToken, 'https://gsx2.apple.com/gsx/api/login');
+
+    plus the user ID that you use to get the initial activation token
+        Set( $AppleGSXUserId,  '<Apple user ID, an email address');
+
     Once you have done this, you can configure the authentication
     information used to connect to GSX via the web UI, at Tools ->
     Configuration -> Assets -> Apple GSX. This menu option is only available
diff --git a/html/Admin/Assets/GSX/index.html b/html/Admin/Assets/GSX/index.html
index 25d5ac7..2b8ce6b 100644
--- a/html/Admin/Assets/GSX/index.html
+++ b/html/Admin/Assets/GSX/index.html
@@ -32,6 +32,11 @@ Unable to connect to the Apple GSX services using the provided account informati
 <tr><td class="label"><label for="KeyFilePath"><&|/l&>Key File Path</&></label></td>
     <td><input name="KeyFilePath" id="KeyFilePath" value="<% $KeyFilePath %>" size="60" /></td>
 </tr>
+<tr><td class="label"><label for="ActivationToken"><&|/l&>Activation Token</&></label></td>
+    <td><input name="ActivationToken" id="ActivationToken" value="<% $ActivationToken %>" size="60" /><br />
+    In case of problem get a new activation token from <a href="<% $get_activation_token_url %>"><% $get_activation_token_url %></a>.</td>
+</tr>
+
 </table>
 
 <& /Elements/Submit, Name => "Update", Label => loc('Update') &>
@@ -42,6 +47,10 @@ $m->clear_and_abort(403) unless $session{'CurrentUser'}->HasRight(
     Right  => 'SuperUser',
 );
 
+my $show_token = RT->Config->Get('AppleGSXShowAuthenticationToken');
+
+my $get_activation_token_url = RT->Config->Get('AppleGSXGetToken');
+
 my $config = RT->System->FirstAttribute('AppleGSXOptions');
 $config = $config ? $config->Content : {};
 if ($ARGS{Update}) {
@@ -51,6 +60,7 @@ if ($ARGS{Update}) {
     $config->{LanguageCode}     = $LanguageCode;
     $config->{CertFilePath}     = $CertFilePath;
     $config->{KeyFilePath}      = $KeyFilePath;
+    $config->{ActivationToken}  = $ActivationToken;
     RT->System->SetAttribute( Name => 'AppleGSXOptions', Content => $config );
 }
 
@@ -61,6 +71,7 @@ my $ok = $config->{UserId}
       && $config->{LanguageCode}
       && $config->{CertFilePath}
       && $config->{KeyFilePath}
+      && $config->{ActivationToken}
       && $gsx->Authenticate;
 
 $UserId           = $config->{UserId};
@@ -69,6 +80,8 @@ $UserTimeZone     = $config->{UserTimeZone} if $config->{UserTimeZone};
 $LanguageCode     = $config->{LanguageCode} if $config->{LanguageCode};
 $CertFilePath     = $config->{CertFilePath};
 $KeyFilePath      = $config->{KeyFilePath};
+$ActivationToken  = $config->{ActivationToken};
+
 </%init>
 <%args>
 $UserId => ""
@@ -77,4 +90,5 @@ $UserTimeZone => "PST"
 $LanguageCode => "en"
 $CertFilePath => ""
 $KeyFilePath => ""
+$ActivationToken => ""
 </%args>
diff --git a/lib/RT/Extension/Assets/AppleGSX.pm b/lib/RT/Extension/Assets/AppleGSX.pm
index 4007c04..4e1a20a 100644
--- a/lib/RT/Extension/Assets/AppleGSX.pm
+++ b/lib/RT/Extension/Assets/AppleGSX.pm
@@ -3,7 +3,7 @@ use warnings;
 package RT::Extension::Assets::AppleGSX;
 use RT::Extension::Assets::AppleGSX::Client;
 
-our $VERSION = '1.2';
+our $VERSION = '2.0';
 
 my $CLIENT;
 my $CLIENT_CACHE;
@@ -27,7 +27,7 @@ sub SerialCF {
 
 sub Fields {
     return RT->Config->Get('AppleGSXMap') || {
-        'Warranty Status'     => 'warrantyStatus',
+        'Warranty Status'     => 'warrantyStatusCode',
         'Warranty Start Date' => 'coverageStartDate',
         'Warranty End Date'   => 'coverageEndDate',
     };
@@ -82,22 +82,18 @@ sub UpdateGSX {
     return (0, "Apple GSX authentication failed; cannot import data")
         unless $CLIENT->Authenticate;
 
-    if ( my $serial = $self->FirstCustomFieldValue($serial_name) ) {
-        my $info = $CLIENT->WarrantyStatus($serial);
-        return (0, "GSX contains no information (check $serial_name?)")
-            unless $info;
-
-        # GSX returns everything in mm/dd/yy format.  Sadly, local'ing
-        # $RT::DateDayBeforeMonth is insufficient (?!).  We set it back,
-        # below; ensure that this function does not return between these
-        # two statements!
-        my $date_order = RT->Config->Get("DateDayBeforeMonth");
-        RT->Config->Set("DateDayBeforeMonth" => 0);
+    if ( my $serial = $self->FirstCustomFieldValue( $serial_name ) ) {
+        my( $ret, $msg, $device ) = $CLIENT->GetDataForSerial( $serial );
+        if( ! $ret ) {
+            return (0, $msg)
+        }
 
         my @results;
         for my $field ( keys %$FIELDS_MAP ) {
             my $old = $self->FirstCustomFieldValue($field);
-            my $new = $info->{warrantyDetailInfo}{ $FIELDS_MAP->{$field} };
+            # data is either at device level or in $device->{warrantyInfo}
+            # the old mapping doesn't know about those 2 levels so we look in both places
+            my $new = $device->{ $FIELDS_MAP->{$field} } || $device->{warrantyInfo}{ $FIELDS_MAP->{$field} };
             if ( defined $new ) {
                 # Canonicalize date and datetime CFs
                 if ($self->LoadCustomFieldByIdentifier($field)->Type =~ /^date(time)?/i) {
@@ -123,8 +119,6 @@ sub UpdateGSX {
             }
         }
 
-        RT->Config->Set("DateDayBeforeMonth" => $date_order);
-
         return (1, @results);
     }
     else {
diff --git a/lib/RT/Extension/Assets/AppleGSX/Client.pm b/lib/RT/Extension/Assets/AppleGSX/Client.pm
index c0e536a..85a45b2 100644
--- a/lib/RT/Extension/Assets/AppleGSX/Client.pm
+++ b/lib/RT/Extension/Assets/AppleGSX/Client.pm
@@ -6,52 +6,48 @@ package RT::Extension::Assets::AppleGSX::Client;
 use Net::SSL;
 use LWP::UserAgent;
 
-use XML::Simple;
-my $xs = XML::Simple->new;
+use JSON;
 
 use base 'Class::Accessor::Fast';
 __PACKAGE__->mk_accessors(
-    qw/UserAgent UserSessionId UserSessionTimeout UserId UserTimeZone
-      ServiceAccountNo LanguageCode CertFilePath KeyFilePath/
+    qw/UserAgent ActivationToken AuthenticationToken UserId UserTimeZone
+      ServiceAccountNo LanguageCode CertFilePath KeyFilePath AppleGSXApiBase/
 );
 
 sub new {
     my $class = shift;
     my $args  = ref $_[0] eq 'HASH' ? shift @_ : {@_};
     my $self  = $class->SUPER::new($args);
+
     $ENV{HTTPS_CERT_FILE} = $self->CertFilePath;
     $ENV{HTTPS_KEY_FILE} = $self->KeyFilePath;
+    my $store_code = sprintf( "%010d", $self->ServiceAccountNo);
+
     $self->UserAgent( LWP::UserAgent->new(ssl_opts => { verify_hostname => 0 }) ) unless $self->UserAgent;
+    my $default_headers = HTTP::Headers->new(
+        'X-Apple-SoldTo' => $store_code,
+        'X-Apple-ShipTo' => $store_code,
+    );
+    $self->UserAgent->default_headers( $default_headers );
+
+    # by default use the testing (-uat) URLs for both the API and getting the initial token
+    $self->{AppleGSXApiBase}  ||= 'https://partner-connect-uat.apple.com/gsx/api';
+    $self->{AppleGSXGetToken} ||= 'https://gsx2-uat.apple.com/gsx/api/login';
+
     return $self;
 }
 
+# may need a name change, this does not authenticate, but just checks that the API is accessible
 sub Authenticate {
     my $self = shift;
 
-    my $xml = $self->PrepareXML(
-        'Authenticate',
-        {
-            userId           => $self->UserId,
-            serviceAccountNo => $self->ServiceAccountNo,
-            languageCode     => $self->LanguageCode,
-            userTimeZone     => $self->UserTimeZone,
-        }
-    );
-
-    my $res = $self->SendRequest($xml);
+    my %headers = ( Accept => 'text/plain' );
+    my $res = $self->UserAgent->get( $self->AppleGSXApiBase . "/authenticate/check", %headers );
     if ( $res->is_success ) {
-        my $ret =
-          $self->ParseResponseXML( 'Authenticate', $res->decoded_content );
-        $self->UserSessionId( $ret->{'userSessionId'} );
-
-        # official timeout is 30 minutes, minus 5 is to avoid potential
-        # out of sync time issue
-        $self->UserSessionTimeout( time() + 25 * 60 );
-        return $self->UserSessionId;
+        return 1;
     }
     else {
-        warn "Failed to authenticate to Apple GSX: " . $res->status_line;
-        warn "Full response: " . $res->content;
+        RT->Logger->error( "Failed to authenticate to Apple GSX: " . $res->status_line );
         return;
     }
 }
@@ -60,91 +56,107 @@ sub WarrantyStatus {
     my $self = shift;
     my $serial = shift or return;
 
-    $self->Authenticate
-      unless $self->UserSessionId && time() < $self->UserSessionTimeout;
+    my( $ret, $msg, $device )= $self->GetDataForSerial( $serial );
+    if( ! $ret ) {
+        return( 0, $msg, undef);
+    }
+    if( ! $device->{warrantyInfo} ) {
+        RT->Logger->warning( "no warantyInfo returned (for sn $serial)" );
+        return( 0, "no warantyInfo returned" );
+    }
+    return ( 1, '', $device->{warrantyInfo});
+}
+
+sub GetDataForSerial {
+    my $self = shift;
+    my $serial = shift or return;
+
+    my $token = $self->AuthenticationToken;
 
-    my $xml = $self->PrepareXML(
-        'WarrantyStatus',
-        {
-            'userSession' => { userSessionId => $self->UserSessionId, },
-            'unitDetail'  => { serialNumber  => $serial,
-                               shipTo        => $self->ServiceAccountNo }
-        }
+    my %headers = (
+        'X-Apple-Auth-Token' => $token,
+        'Content-Type'       => 'application/json',
+        'Accept'             => 'application/json',
     );
 
-    for my $try (1..5) {
-        my $res = $self->SendRequest($xml);
-        unless ($res->is_success) {
-            my $data = eval {$xs->parse_string( $res->decoded_content, NoAttr => 1, SuppressEmpty => undef ) };
-            my $fault = $data ? $data->{"S:Body"}{"S:Fault"}{"faultstring"} : $res->status_line;
-            if ($fault =~ /^The serial number entered has been marked as obsolete/) {
-                # no-op
-            } elsif ($fault =~ /^The serial you entered is not valid/) {
-                # no-op
-            } else {
-                warn "Failed to get Apple GSX warranty status of serial $serial: $fault";
-            }
-            return;
+    my $args = { "device" => { "id" => $serial } };
+    my $json = encode_json( $args );
+    my $response;
+
+    # only try if we have a token, otherwise we need to get one first
+    if( $token) {
+        $response = $self->UserAgent->post( $self->AppleGSXApiBase . "/repair/product/details", Content => $json, %headers );
+    }
+
+    if( ! $token || $response->code == 401 ) {
+        my( $ret, $msg, $new_token );
+        if( $token ) {
+            ( $ret, $msg, $new_token )= $self->get_new_authentication_token( $token );
+        }
+        if( ! $token || ! $ret) {
+            ( $ret, $msg, $new_token)= $self->get_new_authentication_token( $self->ActivationToken );
         }
 
-        my $ret = $self->ParseResponseXML( 'WarrantyStatus', $res->decoded_content );
-        return $ret if $ret->{warrantyDetailInfo} and $ret->{warrantyDetailInfo}{serialNumber};
+        if( $ret) {
+            RT->Logger->debug( "got new authentication token");
+            $headers{'X-Apple-Auth-Token'} = $new_token;
+            $response = $self->UserAgent->post( $self->AppleGSXApiBase . "/repair/product/details", Content => $json, %headers);
+        }
+        else {
+            return ( 0, "error connecting to the GSX API: $msg", undef);
+        }
     }
-    warn "Repeatedly failed to get complete response from Apple GSX for serial $serial";
-    return;
-}
 
-sub PrepareXML {
-    my $self   = shift;
-    my $method = shift;
-    my $args   = shift || {};
-
-    my $xml = $xs->XMLout(
-        {
-            'soapenv:Body' =>
-              { "glob:$method" => { "${method}Request" => $args, }, },
-        },
-        NoAttr   => 1,
-        KeyAttr  => [],
-        RootName => '',
-    );
-    return <<"EOF",
-<?xml version="1.0" encoding="UTF-8"?>
-<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
-xmlns:glob="http://gsxws.apple.com/elements/global">
-<soapenv:Header/>
-$xml
-</soapenv:Envelope>
-EOF
+    if( $response->is_success ) {
+        my $product_details = decode_json( $response->decoded_content );
+        my $device = $product_details->{device};
 
-}
+        # we set a couple of fields that were named differently in the old API, so old code still workd
+        # old warrantyStatus is new warrantyStatusDescription
+        $device->{warrantyInfo}->{warrantyStatus} = $device->{warrantyInfo}->{warrantyStatusDescription};
+        # old estimatedPurchaseDate is new purchaseDate (in warrantyInfo)
+        $device->{estimatedPurchaseDate} = $device->{warrantyInfo}->{purchaseDate};
 
-sub ParseResponseXML {
-    my $self   = shift;
-    my $method = shift;
-    my $xml    = shift;
-    my $ret    = $xs->XMLin( $xml, NoAttr => 1, SuppressEmpty => undef, NSExpand => 1 );
-    return $ret->{'{http://schemas.xmlsoap.org/soap/envelope/}Body'}
-        ->{"{http://gsxws.apple.com/elements/global}${method}Response"}
-        ->{"${method}Response"};
+        return( 1, '', $device);
+    }
+    else {
+        RT->Logger->warning( "Failed to get response from Apple GSX for serial $serial" );
+        return( 0, "Failed to get response from Apple GSX for serial $serial" );
+    }
 }
 
-sub SendRequest {
+sub get_new_authentication_token {
     my $self = shift;
-    my $xml  = shift;
+    my $old_token= shift;
+
+    my $data = { userAppleId => $self->UserId, authToken => $old_token };
+    my $json = encode_json( $data);
+    my %headers = (
+        'Content-Type' => 'application/json',
+        Accept => 'application/json',
+    );
+    my $response = $self->UserAgent->post( $self->AppleGSXApiBase . "/authenticate/token", Content => $json, %headers );
+    if( $response->code == 200 ) {
+        my $json_string = $response->decoded_content;
+        my $response_json = decode_json( $json_string);
 
-    my $domain = 'https://gsxapi.apple.com';
+        my $new_authentication_token = $response_json->{authToken};
 
-    # Apple standard appears to be to use 'Test' for testing environment
-    # certs.
-    $domain = 'https://gsxapiut.apple.com' if $self->CertFilePath =~ /Test/;
+        $self->AuthenticationToken( $new_authentication_token);
 
-    my $res  = $self->UserAgent->post(
-        "$domain/gsx-ws/services/am/asp",
-        'Content-Type' => 'text/xml; charset=utf-8',
-        Content        => $xml,
-    );
-    return $res;
+        # save the token in the AppleGSXOptions attribute
+        my $config= RT->System->FirstAttribute('AppleGSXOptions');
+        my $content = $config->Content;
+        $content->{AuthenticationToken} = $new_authentication_token;
+        # $config->SetContent( $content);
+        RT->System->SetAttribute( Name => 'AppleGSXOptions', Content => $content );
+
+        return ( 1, '', $new_authentication_token);
+    }
+    else {
+        RT->Logger->error( "Failed to get authentication token" );
+        return( 0, "cannot get authentication token: " . $response->code, undef);
+    }
 }
 
 1;

-----------------------------------------------------------------------


More information about the Bps-public-commit mailing list