18
18
19
19
import alfio .manager .system .ConfigurationManager ;
20
20
import alfio .model .Event ;
21
+ import alfio .model .EventAndOrganizationId ;
21
22
import alfio .model .EventDescription ;
22
23
import alfio .model .Ticket ;
23
24
import alfio .model .system .Configuration ;
24
25
import alfio .model .system .ConfigurationKeys ;
25
26
import alfio .model .user .Organization ;
26
- import alfio .repository .EventDescriptionRepository ;
27
- import alfio .repository .EventRepository ;
28
- import alfio .repository .TicketCategoryRepository ;
27
+ import alfio .repository .*;
29
28
import alfio .repository .user .OrganizationRepository ;
30
29
import alfio .util .Json ;
31
30
import com .github .benmanes .caffeine .cache .Cache ;
32
31
import com .github .benmanes .caffeine .cache .Caffeine ;
33
32
import com .ryantenney .passkit4j .Pass ;
34
33
import com .ryantenney .passkit4j .PassResource ;
35
34
import com .ryantenney .passkit4j .PassSerializer ;
36
- import com .ryantenney .passkit4j .model .Color ;
37
- import com .ryantenney .passkit4j .model .TextField ;
38
35
import com .ryantenney .passkit4j .model .*;
39
36
import com .ryantenney .passkit4j .sign .PassSigner ;
40
37
import com .ryantenney .passkit4j .sign .PassSignerImpl ;
41
38
import com .ryantenney .passkit4j .sign .PassSigningException ;
42
39
import lombok .AllArgsConstructor ;
43
40
import lombok .extern .log4j .Log4j2 ;
41
+ import org .apache .commons .lang3 .StringUtils ;
42
+ import org .apache .commons .lang3 .tuple .Pair ;
44
43
import org .imgscalr .Scalr ;
45
44
import org .springframework .core .io .ClassPathResource ;
46
45
import org .springframework .stereotype .Component ;
47
46
48
47
import javax .imageio .ImageIO ;
49
48
import java .awt .image .BufferedImage ;
50
- import java .io .ByteArrayInputStream ;
51
- import java .io .ByteArrayOutputStream ;
52
- import java .io .IOException ;
53
- import java .io .InputStream ;
49
+ import java .io .*;
54
50
import java .time .format .DateTimeFormatter ;
55
51
import java .time .format .FormatStyle ;
56
- import java .util .List ;
57
52
import java .util .*;
58
53
import java .util .concurrent .TimeUnit ;
59
54
import java .util .function .Function ;
55
+ import java .util .stream .Collectors ;
60
56
61
57
import static alfio .model .system .ConfigurationKeys .*;
62
58
63
59
@ Component
64
60
@ AllArgsConstructor
65
61
@ Log4j2
66
- class PassBookManager {
62
+ public class PassKitManager {
67
63
68
- private final Cache <String , Optional <byte []>> passbookLogoCache = Caffeine .newBuilder ()
64
+ private static final String APPLE_PASS = "ApplePass" ;
65
+ private final Cache <String , Optional <byte []>> passKitLogoCache = Caffeine .newBuilder ()
69
66
.maximumSize (20 )
70
67
.expireAfterWrite (20 , TimeUnit .MINUTES )
71
68
.build ();
@@ -75,55 +72,80 @@ class PassBookManager {
75
72
private final FileUploadManager fileUploadManager ;
76
73
private final EventDescriptionRepository eventDescriptionRepository ;
77
74
private final TicketCategoryRepository ticketCategoryRepository ;
75
+ private final TicketRepository ticketRepository ;
76
+ private final TicketReservationRepository ticketReservationRepository ;
78
77
79
78
80
- byte [] getPassBook (Map <String , String > model ) {
79
+ public boolean writePass (Ticket ticket , EventAndOrganizationId event , OutputStream out ) throws IOException , PassSigningException {
80
+ Organization organization = organizationRepository .getById (event .getOrganizationId ());
81
+ Map <ConfigurationKeys , String > passConf = getConfigurationKeys (event );
82
+ if (!passConf .isEmpty ()) {
83
+ buildPass (ticket , eventRepository .findById (event .getId ()), organization , passConf , out );
84
+ return true ;
85
+ } else {
86
+ log .trace ("Cannot generate Pass. Missing configuration keys, check if all 5 are presents" );
87
+ return false ;
88
+ }
89
+ }
90
+
91
+ byte [] getPass (Map <String , String > model ) {
81
92
try {
82
93
Ticket ticket = Json .fromJson (model .get ("ticket" ), Ticket .class );
83
94
int eventId = ticket .getEventId ();
84
95
Event event = eventRepository .findById (eventId );
85
96
Organization organization = organizationRepository .getById (Integer .valueOf (model .get ("organizationId" ), 10 ));
86
97
87
- Function <ConfigurationKeys , Configuration .ConfigurationPathKey > partial = Configuration .from (event );
88
- Map <ConfigurationKeys , Optional <String >> pbookConf = configurationManager .getStringConfigValueFrom (
89
- partial .apply (PASSBOOK_TYPE_IDENTIFIER ),
90
- partial .apply (PASSBOOK_KEYSTORE ),
91
- partial .apply (PASSBOOK_KEYSTORE_PASSWORD ),
92
- partial .apply (PASSBOOK_TEAM_IDENTIFIER ),
93
- partial .apply (PASSBOOK_PRIVATE_KEY_ALIAS ));
98
+ Map <ConfigurationKeys , String > passConf = getConfigurationKeys (event );
94
99
//check if all are set
95
- if (pbookConf . values (). stream (). anyMatch ( Optional :: isEmpty )) {
100
+ if (passConf . isEmpty ( )) {
96
101
log .trace ("Cannot generate Passbook. Missing configuration keys, check if all 5 are presents" );
97
102
return null ;
98
103
}
99
104
100
- //
101
- String teamIdentifier = pbookConf .get (PASSBOOK_TEAM_IDENTIFIER ).orElseThrow ();
102
- String typeIdentifier = pbookConf .get (PASSBOOK_TYPE_IDENTIFIER ).orElseThrow ();
103
- byte [] keystoreRaw = Base64 .getDecoder ().decode (pbookConf .get (PASSBOOK_KEYSTORE ).orElseThrow ());
104
- String keystorePwd = pbookConf .get (PASSBOOK_KEYSTORE_PASSWORD ).orElseThrow ();
105
- String privateKeyAlias = pbookConf .get (PASSBOOK_PRIVATE_KEY_ALIAS ).orElseThrow ();
106
-
107
- return buildPass (ticket , event , organization , teamIdentifier , typeIdentifier , keystorePwd , privateKeyAlias , new ByteArrayInputStream (keystoreRaw ));
105
+ try (ByteArrayOutputStream out = new ByteArrayOutputStream ()){
106
+ buildPass (ticket , event , organization , passConf , out );
107
+ return out .toByteArray ();
108
+ }
108
109
} catch (Exception ex ) {
109
110
log .warn ("Got Exception while generating Passbook. Please check configuration." , ex );
110
111
return null ;
111
112
}
112
113
}
113
114
114
- private byte [] buildPass (Ticket ticket ,
115
- Event event ,
116
- Organization organization ,
117
- String teamIdentifier ,
118
- String typeIdentifier ,
119
- String keystorePwd ,
120
- String privateKeyAlias ,
121
- InputStream keyStore ) throws IOException , PassSigningException {
115
+ private Map <ConfigurationKeys , String > getConfigurationKeys (EventAndOrganizationId event ) {
116
+ Function <ConfigurationKeys , Configuration .ConfigurationPathKey > partial = Configuration .from (event );
117
+ var configValues = configurationManager .getStringConfigValueFrom (
118
+ partial .apply (PASSBOOK_TYPE_IDENTIFIER ),
119
+ partial .apply (PASSBOOK_KEYSTORE ),
120
+ partial .apply (PASSBOOK_KEYSTORE_PASSWORD ),
121
+ partial .apply (PASSBOOK_TEAM_IDENTIFIER ),
122
+ partial .apply (PASSBOOK_PRIVATE_KEY_ALIAS )
123
+ );
124
+ if (configValues .values ().stream ().anyMatch (Optional ::isEmpty )) {
125
+ return Map .of ();
126
+ }
127
+
128
+ return configValues
129
+ .entrySet ()
130
+ .stream ()
131
+ .collect (Collectors .toMap (Map .Entry ::getKey , e -> e .getValue ().orElseThrow ()));
132
+ }
133
+
134
+ private void buildPass (Ticket ticket ,
135
+ Event event ,
136
+ Organization organization ,
137
+ Map <ConfigurationKeys , String > config ,
138
+ OutputStream out ) throws IOException , PassSigningException {
122
139
123
140
// from example: https://github.com/ryantenney/passkit4j/blob/master/src/test/java/com/ryantenney/passkit4j/EventTicketExample.java
124
141
// specification: https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/PassKit_PG/Creating.html#//apple_ref/doc/uid/TP40012195-CH4-SW6
125
142
126
143
var ticketLocale = Locale .forLanguageTag (ticket .getUserLanguage ());
144
+ String teamIdentifier = config .get (PASSBOOK_TEAM_IDENTIFIER );
145
+ String typeIdentifier = config .get (PASSBOOK_TYPE_IDENTIFIER );
146
+ byte [] keystoreRaw = Base64 .getDecoder ().decode (config .get (PASSBOOK_KEYSTORE ));
147
+ String keystorePwd = config .get (PASSBOOK_KEYSTORE_PASSWORD );
148
+ String privateKeyAlias = config .get (PASSBOOK_PRIVATE_KEY_ALIAS );
127
149
128
150
Location loc = new Location (Double .parseDouble (event .getLatitude ()), Double .parseDouble (event .getLongitude ())).altitude (0D );
129
151
String eventDescription = eventDescriptionRepository .findDescriptionByEventIdTypeAndLocale (event .getId (), EventDescription .EventDescriptionType .DESCRIPTION , ticket .getUserLanguage ()).orElse ("" );
@@ -134,6 +156,8 @@ private byte[] buildPass(Ticket ticket,
134
156
.groupingIdentifier (organization .getEmail ())
135
157
.description (event .getDisplayName ())
136
158
.serialNumber (ticket .getUuid ())
159
+ .authenticationToken (buildAuthenticationToken (ticket , event , event .getPrivateKey ()))
160
+ .webServiceURL (StringUtils .removeEnd (configurationManager .getRequiredValue (Configuration .getSystemConfiguration (BASE_URL )), "/" ) + "/api/pass/event/" + event .getShortName () +"/" )
137
161
.relevantDate (Date .from (event .getBegin ().toInstant ()))
138
162
.expirationDate (Date .from (event .getEnd ().toInstant ()))
139
163
.locations (loc )
@@ -169,7 +193,7 @@ private byte[] buildPass(Ticket ticket,
169
193
170
194
fileUploadManager .findMetadata (event .getFileBlobId ()).ifPresent (metadata -> {
171
195
if (metadata .getContentType ().equals ("image/png" ) || metadata .getContentType ().equals ("image/jpeg" )) {
172
- Optional <byte []> cachedLogo = passbookLogoCache .get (event .getFileBlobId (), (id ) -> {
196
+ Optional <byte []> cachedLogo = passKitLogoCache .get (event .getFileBlobId (), (id ) -> {
173
197
ByteArrayOutputStream baos = new ByteArrayOutputStream ();
174
198
fileUploadManager .outputFile (event .getFileBlobId (), baos );
175
199
return readAndConvertImage (baos );
@@ -181,17 +205,49 @@ private byte[] buildPass(Ticket ticket,
181
205
});
182
206
183
207
pass .files (passResources .toArray (new PassResource [0 ]));
184
-
185
- try (ByteArrayOutputStream baos = new ByteArrayOutputStream ();
186
- InputStream appleCert = new ClassPathResource ("/alfio/certificates/AppleWWDRCA.cer" ).getInputStream ()) {
208
+ try (InputStream appleCert = new ClassPathResource ("/alfio/certificates/AppleWWDRCA.cer" ).getInputStream ()) {
187
209
PassSigner signer = PassSignerImpl .builder ()
188
- .keystore (keyStore , keystorePwd )
210
+ .keystore (new ByteArrayInputStream ( keystoreRaw ) , keystorePwd )
189
211
.alias (privateKeyAlias )
190
212
.intermediateCertificate (appleCert )
191
213
.build ();
192
- PassSerializer .writePkPassArchive (pass , signer , baos );
193
- return baos .toByteArray ();
214
+ PassSerializer .writePkPassArchive (pass , signer , out );
215
+ }
216
+ }
217
+
218
+ private String buildAuthenticationToken (Ticket ticket , EventAndOrganizationId event , String privateKey ) {
219
+ var code = event .getId () + "/" + ticket .getTicketsReservationId () + "/" + ticket .getUuid ();
220
+ return Ticket .hmacSHA256Base64 (privateKey , code );
221
+ }
222
+
223
+ public Optional <Pair <EventAndOrganizationId , Ticket >> validateToken (String eventName , String typeIdentifier , String ticketUuid , String authorizationHeader ) {
224
+ String token ;
225
+ if (authorizationHeader .startsWith (APPLE_PASS )) {
226
+ // From the specs:
227
+ // The Authorization header is supplied; its value is the word ApplePass, followed by a space,
228
+ // followed by the pass’s authorization token as specified in the pass.
229
+ token = authorizationHeader .substring (APPLE_PASS .length () + 1 );
230
+ } else {
231
+ log .trace ("Authorization Header does not start with ApplePass" );
232
+ return Optional .empty ();
233
+ }
234
+
235
+ var eventOptional = eventRepository .findOptionalEventAndOrganizationIdByShortName (eventName );
236
+ if (eventOptional .isEmpty ()) {
237
+ log .trace ("event {} not found" , eventName );
238
+ return Optional .empty ();
239
+ }
240
+
241
+ var event = eventOptional .get ();
242
+ var typeIdentifierOptional = configurationManager .getStringConfigValue (Configuration .from (event , PASSBOOK_TYPE_IDENTIFIER ));
243
+ if (typeIdentifierOptional .isEmpty () || !typeIdentifierOptional .get ().equals (typeIdentifier )) {
244
+ log .trace ("typeIdentifier does not match. Expected {}, got {}" , typeIdentifierOptional .orElse ("not-found" ), typeIdentifier );
245
+ return Optional .empty ();
194
246
}
247
+ return ticketRepository .findOptionalByUUID (ticketUuid )
248
+ .filter (t -> t .getEventId () == event .getId ())
249
+ .filter (t -> buildAuthenticationToken (t , event , eventRepository .getPrivateKey (event .getId ())).equals (token ))
250
+ .map (t -> Pair .of (event , t ));
195
251
}
196
252
197
253
private void addLogoResources (byte [] logo , List <PassResource > passResources ) {
0 commit comments