DRAFT Reverse Engineering Lidl Fun Trips

  • Apache Cordova
  • AngularJS
  • Ionic

L'appli est une simple webapp qui fait une serie d'appels a un backend rest.

Url du site: https://lidlfuntrips.be/

Je suis une fénéant et j'aime postman. Voici un fichier qui contient toutes les query interessantes a charger dans postman.

J'utilise l'env var { {bearer} }. Il suffit de faire un call a login pour recup le bearer et mettre a jour la var d'env, j'ai la flemme de le faire pour vous. Je le ferai un jour peut être.

Et vu que j'ai pas de manière d'heberger le fichier de manière sure fiable sur la longeur je vous le file ici, copiez collez et ROTFL sur mon sujet

{
	"id": "0ac5fb09-d46b-5855-aa1e-8397ee0386f6",
	"name": "Lidle",
	"description": "",
	"order": [
		"7f95e59e-5c21-9ce7-361e-b12f1c9bd0f4",
		"89d86de1-4ef4-5af1-cad4-429e3ca18c67",
		"e46ed2b5-7747-bf0d-5cd1-2498397e470e",
		"e525b5d4-6ce9-3442-bb37-f02b9c95d50b",
		"a7eb7921-622c-491c-b08f-26b6e8f098e4"
	],
	"folders": [],
	"folders_order": [],
	"timestamp": 1526791518755,
	"owner": 0,
	"public": false,
	"requests": [
		{
			"id": "7f95e59e-5c21-9ce7-361e-b12f1c9bd0f4",
			"headers": "Content-Type: application/json\nAuthorization: Basic Og==\n",
			"headerData": [
				{
					"key": "Content-Type",
					"value": "application/json",
					"description": "",
					"enabled": true
				},
				{
					"key": "Authorization",
					"value": "Basic Og==",
					"description": "",
					"enabled": true
				}
			],
			"url": "https://wallet-service.qup.eu/v1/users/login",
			"queryParams": [],
			"pathVariables": {},
			"pathVariableData": [],
			"preRequestScript": null,
			"method": "POST",
			"collectionId": "0ac5fb09-d46b-5855-aa1e-8397ee0386f6",
			"data": [],
			"dataMode": "raw",
			"name": "Login",
			"description": "",
			"descriptionFormat": "html",
			"time": 1526791551026,
			"version": 2,
			"responses": [],
			"tests": null,
			"currentHelper": "normal",
			"helperAttributes": {},
			"rawModeData": "{\n\t\"device_id\": 1234567890,\n\t\"plateform\": \"android\",\n\t\"grant_type\": \"password\",\n\t\"client_id\": \"[email protected]\",\n\t\"client_secret\": \"elAsNeAD1j1hvPwfr5t0iLBxq2mXLZ0M8CWmabzs\",\n\t\"username\": \"[email protected]\",\n\t\"password\": \"Radislav250399\"\n}"
		},
		{
			"id": "89d86de1-4ef4-5af1-cad4-429e3ca18c67",
			"headers": "Authorization: Bearer NSx5kamSYNYCBsmgCipO49wqzzQ2ZRwHmZvukKMC\n",
			"headerData": [
				{
					"key": "Authorization",
					"value": "Bearer NSx5kamSYNYCBsmgCipO49wqzzQ2ZRwHmZvukKMC",
					"description": "",
					"enabled": true
				}
			],
			"url": "https://wallet-service.qup.eu/v1/me/messages",
			"queryParams": [],
			"pathVariables": {},
			"pathVariableData": [],
			"preRequestScript": null,
			"method": "GET",
			"collectionId": "0ac5fb09-d46b-5855-aa1e-8397ee0386f6",
			"data": null,
			"dataMode": "params",
			"name": "Get Message",
			"description": "",
			"descriptionFormat": "html",
			"time": 1526791538316,
			"version": 2,
			"responses": [],
			"tests": null,
			"currentHelper": "normal",
			"helperAttributes": {}
		},
		{
			"id": "a7eb7921-622c-491c-b08f-26b6e8f098e4",
			"headers": "Authorization: Bearer {{bearer}}\nContent-Type: application/json\n",
			"headerData": [
				{
					"key": "Authorization",
					"value": "Bearer {{bearer}}",
					"description": "",
					"enabled": true
				},
				{
					"key": "Content-Type",
					"value": "application/json",
					"description": "",
					"enabled": true
				}
			],
			"url": "https://wallet-service.qup.eu/v1/me/vouchers/add",
			"queryParams": [],
			"pathVariables": {},
			"pathVariableData": [],
			"preRequestScript": null,
			"method": "POST",
			"collectionId": "0ac5fb09-d46b-5855-aa1e-8397ee0386f6",
			"data": [],
			"dataMode": "raw",
			"name": "Post Vaucher",
			"description": "",
			"descriptionFormat": "html",
			"time": 1526792030344,
			"version": 2,
			"responses": [],
			"tests": null,
			"currentHelper": "normal",
			"helperAttributes": {},
			"rawModeData": "{\r\n\"code\":\"LIA25636QZ\"\r\n}"
		},
		{
			"id": "e46ed2b5-7747-bf0d-5cd1-2498397e470e",
			"headers": "Authorization: Bearer MqyGajuFyVAUfJgNTfdLK0AOsWqBTCc11CGLP1yH\n",
			"headerData": [
				{
					"key": "Authorization",
					"value": "Bearer MqyGajuFyVAUfJgNTfdLK0AOsWqBTCc11CGLP1yH",
					"description": "",
					"enabled": true
				}
			],
			"url": "https://wallet-service.qup.eu/v1/me",
			"queryParams": [],
			"pathVariables": {},
			"pathVariableData": [],
			"preRequestScript": null,
			"method": "GET",
			"collectionId": "0ac5fb09-d46b-5855-aa1e-8397ee0386f6",
			"data": null,
			"dataMode": "params",
			"name": "Get Profile",
			"description": "",
			"descriptionFormat": "html",
			"time": 1526791618943,
			"version": 2,
			"responses": [],
			"tests": null,
			"currentHelper": "normal",
			"helperAttributes": {}
		},
		{
			"id": "e525b5d4-6ce9-3442-bb37-f02b9c95d50b",
			"headers": "Authorization: Bearer {{bearer}}\n",
			"headerData": [
				{
					"key": "Authorization",
					"value": "Bearer {{bearer}}",
					"description": "",
					"enabled": true
				}
			],
			"url": "https://wallet-service.qup.eu/v1/me/vouchers",
			"queryParams": [],
			"pathVariables": {},
			"pathVariableData": [],
			"preRequestScript": null,
			"method": "GET",
			"collectionId": "0ac5fb09-d46b-5855-aa1e-8397ee0386f6",
			"data": null,
			"dataMode": "params",
			"name": "Get encoded voucher",
			"description": "",
			"descriptionFormat": "html",
			"time": 1526791836364,
			"version": 2,
			"responses": [],
			"tests": null,
			"currentHelper": "normal",
			"helperAttributes": {}
		}
	]
}

Authentification des requêtes via le header HTTP Authorization Bearer. Chaque requête doit contenir le token.

Le token peut être obtenu en faisait un POST sur /users/login (UserProvider.Login())

Query

      curl -X POST \
  https://wallet-service.qup.eu/v1/users/login \
  -H 'content-type: application/json' \
  -d '{
	"device_id": 1234567890,
	"plateform": "android",
	"grant_type": "password",
	"client_id": "[email protected]",
	"client_secret": "elAsNeAD1j1hvPwfr5t0iLBxq2mXLZ0M8CWmabzs",
	"username": "toto",
	"password": "1234"
}'

Réponse

{
	"access_token": "zlsTbxIbDpdUr27vmrJQSgaUPj4NcdcU7d3XwdgI",
	"token_type": "Bearer",
	"expires_in": 30758400
}

A noter que le ContentPageProvider utilise un autre mécanisme d'authentification, cfr doc du provider.

  • Le mot de passe doit faire minimum 6 caractères, contenir une majuscule, minuscule et nombre
  • L'adresse mail doit répondre au format (désolé pour le regex du pauvre) [A-Zaz0-9]@[A-Zaz0-9].[A-Zaz0-9]

Query

curl -X POST \
  https://wallet-service.qup.eu/v1/users/register \
  -H 'cache-control: no-cache' \
  -H 'content-type: application/json' \
  -d '{
	"title": "f",
	"firs_name": "AZERTYU",
	"suffix": "AZERTY",
	"last_name": "AZERTYU2",
	"name": "AZERTYU AZERTYU",
	"postalcode": "4000",
	"email": "[email protected]",
	"password": "azerty1234$A",
	"device_id": 1234567890,
	"plateform": "android",
	"grant_type": "password",
	"client_id": "[email protected]",
	"client_secret": "elAsNeAD1j1hvPwfr5t0iLBxq2mXLZ0M8CWmabzs"
}'

Response

{
	"access_token": "kVSU7NPBrYP3meb4XtvQfq3JwbfH7P0htnmGph4J",
	"token_type": "Bearer",
	"expires_in": 30758400
}

Query

curl -X POST \
  https://wallet-service.qup.eu/v1/me/vouchers/add \
  -H 'authorization: Bearer kVSU7NPBrYP3meb4XtvQfq3JwbfH7P0htnmGph4J' \
  -H 'content-type: application/json' \
  -d '{
"code":"LIA25636QZ"
}'

Response (erreur - inexistant)

{
	"internal_code": 105,
	"success": false,
	"errors": [
		""
	]
}

Response (erreur - déja encodé)

{
	"internal_code": 101,
	"success": false,
	"errors": [
		""
	]
}

Responses (succes)

{
	"internal_code": 0,
	"success": true,
	"data": {
		"voucher": "XKN5GWWDCE",
		"order_id": null,
		"used": false
	}
}

Query

curl -X GET \
  https://wallet-service.qup.eu/v1/me/vouchers \
  -H 'authorization: Bearer Ad1GI7gG39DKJiijewuNUn019RZclLewSTFVolJt'

Response

{
	"internal_code": 0,
	"success": true,
	"data": [
		{
			"voucher": "D6G279P2KB",
			"used": false,
			"order_id": null
		},
		{
			"voucher": "3Z9LAXDM12",
			"used": false,
			"order_id": null
		},
		// Une liste quoi...
	]
}

Query

curl -X GET \
  https://wallet-service.qup.eu/v1/me/ \
  -H 'authorization: Bearer Ad1GI7gG39DKJiijewuNUn019RZclLewSTFVolJt'

Response

{
	"internal_code": 0,
	"success": true,
	"data": {
		"name": "AZERTYU AZERTYU",
		"email": "[email protected]",
		"postalcode": "4000",
		"created_at": "2018-05-18 16:04:56GMT+0000",
		"temp_password": null,
		"locale": "en_US",
		"first_name": "",
		"suffix": "AZERTY",
		"last_name": "AZERTYU2",
		"title": "f",
		"telephone": null
	}
}

Query

curl -X GET \
  https://wallet-service.qup.eu/v1/me/messages \
  -H 'authorization: Bearer NSx5kamSYNYCBsmgCipO49wqzzQ2ZRwHmZvukKMC'

Response

{
	"internal_code": 0,
	"success": true,
	"data": [
		{
			"code": "cVR4ckSGLNc:APA91bFqUUafMNVfIphHlBaisTVBQQCtM8TSck5jOhaS8LZanKaxj-8tKM7B-AH2pZj_RqgqFw2CzlHnxkGKEH3vFoxdhyh9nF4uKMqUWaxAcBI4HTgG_8rdi5FZZT4ZKsA9KnxIX5Tx",
			"title": "Namens Lidl: Fijne Pinksteren!",
			"message": "De la part de Lidl Funtrips: joyeuse fête de la Pentecôte!",
			"read": false,
			"created_at": "2018-05-19 10:55:59GMT+0000"
		},
		{
			"code": "cVR4ckSGLNc:APA91bFqUUafMNVfIphHlBaisTVBQQCtM8TSck5jOhaS8LZanKaxj-8tKM7B-AH2pZj_RqgqFw2CzlHnxkGKEH3vFoxdhyh9nF4uKMqUWaxAcBI4HTgG_8rdi5FZZT4ZKsA9KnxIX5Tx",
			"title": "€ 10 korting bij Bobbejaanland met Pinksteren!",
			"message": "Une réduction de 10 € chez Bobbejaanland à la Pentecôte! ",
			"read": false,
			"created_at": "2018-05-16 13:09:23GMT+0000"
		},
		{
			"code": "cVR4ckSGLNc:APA91bFqUUafMNVfIphHlBaisTVBQQCtM8TSck5jOhaS8LZanKaxj-8tKM7B-AH2pZj_RqgqFw2CzlHnxkGKEH3vFoxdhyh9nF4uKMqUWaxAcBI4HTgG_8rdi5FZZT4ZKsA9KnxIX5Tx",
			"title": "Ga met Hemelvaart de baby-olifantjes bezoeken in Planckendael!",
			"message": "Allez visiter les bébés éléphants à Planckendael pendant l'Ascension!",
			"read": false,
			"created_at": "2018-05-09 14:55:05GMT+0000"
		}
	]
}
Endpoint Method
/me GET, PUT, DELETE
/me/messages GET, PUT
/me/vouchers GET
/me/vouchers/add POST
/me/messages GET, PUT
/users/login POST
/users/register POST
/users/forgot POST
/content GET
/content/{key} GET

Tout se trouve dans /assets/www/build/

Fichier Role
vendor.js OSEF, angular js
polyfills.js OSEF aussi
swtoolbox.js OSEF toujours
main.js Toute la logique est ici
0.js ScanPageModule
1.js ProfilePageModule
2.js PassForgotPageModule
3.js ManualCodePageModule
4.js LoginPageModule
5.js PrivacyPageModule
6.js PassForgotFeedbackPageModule
7.js MessagesPageModule
8.js IntroPageModule
9.js DeleteProfileModalModule
10.js ContactPageModule

Quelques remarques:

  • Les modules ne contiennent pas de logique autre que UI, ils utilisent des objet provider defini dans main qui eux contiennent la logique, servent de facade, repository, etc…
  • Les pages utilisent les forms de AngularJS, il y a un peu de RE a faire sur les pages pour avoir les objet envoyé.
  • AngularJS utilise beaucoup l'injection de de dépendances, tout est injecté via le constructeur.
  • Chaque module ne contient qu'une page.

ApiService

Methode Doc
getLocalizedClientId() Interface pour AppSettings, utilisé pour build les request
getClientSecret() Interface pour AppSettings, utilisé pour build les request

ApiGateway

Toutes les requêtes REST passent par ce service.

Expose POST, GET, PUT, etc. Toutes les méthodes appellent derrière request(). Cette méthode construit la request rest et set le header Authorization avec le bon token, c'est a peut près tout ce qu'il y a d’intéressant ici.

LocaleService

Retourne nl_NL ou fr_FR, utilise pour build le client_id

ProjectAppSettings

Have Fun, tout est assez clair.

  ProjectAppSettings.ENDPOINT = {
      TEST: {
          BASE_URL: 'https://wallet-service.test.qup.eu/v1/',
          CLIENT_ID: {
              'IOS': 'lidl-<locale>[email protected]',
              'ANDROID': 'lidl-<locale>[email protected]'
          },
          CLIENT_SECRET: {
              'IOS': 'elAsNeAD1j1hvPwfr5t0iLBxq2mXLZ0M8CWmabzs',
              'ANDROID': 'elAsNeAD1j1hvPwfr5t0iLBxq2mXLZ0M8CWmabzs'
          },
          SHOP_URL: 'https://lidlfuntrips.qup.uat1.aanzee.cc'
      },
      LIVE: {
          BASE_URL: 'https://wallet-service.qup.eu/v1/',
          CLIENT_ID: {
              'IOS': 'lidl-<locale>[email protected]',
              'ANDROID': 'lidl-<locale>[email protected]'
          },
          CLIENT_SECRET: {
              'IOS': 'elAsNeAD1j1hvPwfr5t0iLBxq2mXLZ0M8CWmabzs',
              'ANDROID': 'elAsNeAD1j1hvPwfr5t0iLBxq2mXLZ0M8CWmabzs'
          },
          SHOP_URL: 'https://lidlfuntrips.be'
      }
  };
  ProjectAppSettings.SERVER = ProjectAppSettings.ENDPOINT.LIVE;
  ProjectAppSettings.PROJECT = 'lidl';
  ProjectAppSettings.VOUCHER_MASK = [/[a-zA-Z0-9]/, /[a-zA-Z0-9]/, /[a-zA-Z0-9]/, /[a-zA-Z0-9]/, /[a-zA-Z0-9]/, /[a-zA-Z0-9]/, /[a-zA-Z0-9]/, /[a-zA-Z0-9]/, /[a-zA-Z0-9]/, /[a-zA-Z0-9]/];
  ProjectAppSettings.HAS_NAME_SPLIT = true;
  ProjectAppSettings.API_ENDPOINT = ProjectAppSettings.SERVER.BASE_URL;
  ProjectAppSettings.SHOP_URL = ProjectAppSettings.SERVER.SHOP_URL;
  ProjectAppSettings.ANDROID_SENDER_ID = '430671127661';
  ProjectAppSettings.VOUCHER_VALUE = 2;
  ProjectAppSettings.CLIENT_ID = {
      'IOS': ProjectAppSettings.SERVER.CLIENT_ID.IOS,
      'ANDROID': ProjectAppSettings.SERVER.CLIENT_ID.ANDROID
  };
  ProjectAppSettings.CLIENT_SECRET = {
      'IOS': ProjectAppSettings.SERVER.CLIENT_SECRET.IOS,
      'ANDROID': ProjectAppSettings.SERVER.CLIENT_SECRET.ANDROID
  };
  ProjectAppSettings.GA_ID = 'UA-2367280-26';
  ProjectAppSettings.VALUTA = 'EUR';
  ProjectAppSettings.isContentAccessTokenRequired = true;
  ProjectAppSettings.scanditLicenceKey = {
      ios: 'AVwrDYiBHE1gFCB1uhW4ICsgS+avHh6XEnHDO5hQsMErS7UolXsALqohduCGfldXnU/BAxojgEKDfl8zF2pvsxJXazqWVxJW2GIkv4hhqnStawfmiWDcO9xDh8IObnMk8wagkxkXNeTfB1LQ/x0SWoIvRAWtw1RFUbigMVJsbSLIH7ERDf8nMHIiCor+4Mi8KPJ2pj9GtjYklSjesjSk2d9Td3kw/86f16aFSPvL6qmFTsyK04PSo+zeV9rvYIfu2aigvQC0V30mGjnwrB5Cjg6fCVSyBoqKCpl90G+tXo/s6C3T5kPAUpVkAD7V1OZs43W0AJhzG82qNO1Jdh8G8ZmywQhFMWI/i80no4fU+qGNDzvrJr8X59Q3izDbTdCCZ20P1iXQCNXScJn/vpiuAjQyEPSrHybJ/69pDm1oYYP6KxHg+Hyjn2KtukMqJxpXSlcduwB7YrcSnVJSU/YLULUZANDKxt15FCKUpNwg/bBss45yryRh5eAv6LOSGyBZ6FAxwcxSQ+Em5DGqX8dN72dd9m8wHZfco9qIOSl3SiY3GZWivSqK/Wvca+hJ60CtFGuaf34JpKYIELfInznSTGC90VxSMpqRkbch0eCKh/mA4DcdmzuZKDz1jxbnFuWfCSK3y6T42gck+r/KrAixdtbo1inRD+ILKqmc7paZzBa1UUkSx6PvqB1vLa1smqTWMii1hOASKyVj7Nju9TxjqFuidfQcuhvl9f1d5njr62zW0jGB7PqWfKealcvLBReSqgcnghbRQQuSje/xTe8Ou2Ssc/sQNpKafaGjdqC3ucezwzTukj8=',
      android: 'AXBrj7WBIUINN103cBKCgjQPNEdxOLC/3F3tyddJn1zdaZMasEGF6v0vUVobRJq5zmrnrnpbXF1UbIrgPUMKaoZB628XZHMOhHMoNud1xQtNTrdQD3pauIp28UvaTId/PSOuGGAE6gchxM+MvXFTU+4+OzmZM5kgo8Gx+ob2Ok6TZ7VBmk0XTVCtU6ZbHs9Cax9b/njJLjvDHRK5PErlFGjtSq7/gWN6IyjPGqcYEhPeepz+MpCXluOgjHlU1JUh3laLUjLYnMP5i10gMW5nWwkzv9DZfwbeLh3+2Sg90EPWlyBsqekHRXuze7Ams74cmYh3rsraTCucGTKyCgyqKFzLzb5vR7u7tbsq3vTxLQ87xfjRzx3CNUY37TW9NBNHUYWWfc2BgL7b/jOneWxrkFC1MjLahWn44d6mkEM6bxI6UAW5zjk1T48g7D6ALo15sNrDFJ8aohS+qUSXd6Z73uRJ6F9yBbNCBeMZfrgh8eyMyq0Cb1JaXdMDBq+h+maLZr3ySbi+sCZtoaGbrUd5+7IVk7UmkhKiYQntl+gzIoI089hY0qGYLy6hvbzpS1NbiJn6F9QwjMV4w7FxH242jqv5+kbx/5T80ecKxIHdjXEeZYQ+5+aCfYSfmOucDxGVZJVGjh4J+7EJT0Zl0dts++pwXW2vIT/wKQfM+BM7pt8KAeltegl/+33OgIPDsptJ+XKiOA48Wlhb7tly8j78yAsLdzW39Xt/Btw9UTvJdrUqp8YrzeUGCXup4te6YCB+Vn0W302o/Z4GSSvyoIQNHwlxyc2qW4lFS4+wJHhes8ZX4ZjdJGo=',
  };
  ProjectAppSettings.availableLocales = [{
          iso: 'nl',
          locale: 'nl_NL',
      }, {
          iso: 'fr',
          locale: 'fr_FR',
      }];
  ProjectAppSettings.defaultLanguage = 'fr';
  ProjectAppSettings.hasIntroImageBackground = false;
  ProjectAppSettings.contentPages = {
      contact: 'contact',
      privacy: 'privacy',
  };

AppSettings

Interface sur ProjectAppSettings

Providers

UserProvider

Method Doc
login(post_data) POST sur /users/login + LoginPage form data
register(post_data) POST sur /users/register + LoginPage register form data
forgot(post_data) POST sur /users/forgot + LoginPage form data
getProfile() GET sur /user
updateProfile(post_data) PUT sur /user + user object
deleteProfile(post_data) DELETE sur /user + user object
saveExpirationDate()

VoucherProvider

Method Doc
getVoucher() GET sur /me/vouchers
addVoucher(code) POST sur /me/vouchers/add

MessageProvider

Method Doc
getMessage() GET sur /me/messages
markAsRead() PUT sur /me/messages/{messageCode}
convertServerToPushNotification();
incrementNotificationBadge();
getNotificationBadge();
markNotificationsRead();

ContentPageProvider

Récupére du contenu depuis le webservice mais utilise un autre système d'authentification si isContentAccessTokenRequired est a true (ce qui est le cas).

getAccessToken() POST sur /client/access_token avec un grant_type 'client_credentials' pour récupérer un token qui sera ensuite utilisé dans getContentPageForKey(key) qui GET sur /content/{key}

Autres providers dans main.js

Method Doc
StorageProvider Abstraction local storage
PushNotificationProvider Hum… If you don't get the role from service class you're damn to hell
GAProvider Google Analytics

Trucs moins interessants

Modules dans main.js

  • AppModule
  • SharedModule
  • ComponentsModule
  • ServiceModule
  • ProvidersModule
  • PipesModule

Autres classes dans main.js

  • WebPopup
  • QRScanner
  • MyMissingTranslationHandler
  • CustomDate
  • VoucherAmount
  • ScrollShadow
  • SelectSwitchComponent
  • CacheRequest
  • MyApp