Skip to content

Commit

Permalink
force new UUID, force UA attribution
Browse files Browse the repository at this point in the history
Force Medtronic to log requests from nightscout-connect.
Do not allow plausible deniability.
Per nightscout/minimed-connect-to-nightscout#46, force
putting a second UUID in a header.  x-axios-tracing already sets a UUID.
This patch correctly uses browser based headers for processing and requesting
HTML Forms.
  • Loading branch information
bewest committed Aug 21, 2023
1 parent b03f358 commit 94f6436
Showing 1 changed file with 71 additions and 21 deletions.
92 changes: 71 additions & 21 deletions lib/sources/minimedcarelink/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
var qs = require('qs');
var url = require('url');
var tough = require('tough-cookie');
var crypto = require('crypto');

var ACS = require('axios-cookiejar-support');
var software = require('../../../package.json');
//var user_agent_string = [software.name, `${software.name}@${software.version}`, 'M2M@V6', software.homepage].join(', ');
var user_agent_string = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.85 Safari/537.36 Edg/90.0.818.46";
var software_string = [software.name, `${software.name}/${software.version}`, '(M2M@V6)', software.homepage].join(' ');
var browser_string = `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.85 Safari/537.36 Edg/90.0.818.46 ${software.name}/${software.version} (M2M/V6)`;
var user_agent_string = software_string;

// https://github.com/NightscoutFoundation/xDrip/blob/990df119a8404cff56cb68b92a7e0bb640da95ef/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/client/CareLinkClient.java#L559
// https://github.com/nightscout/minimed-connect-to-nightscout/blob/master/carelink.js
Expand Down Expand Up @@ -221,7 +223,7 @@ function carelinkSource (opts, axios) {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9'
, 'Accept-Language': "en;q=0.9, *;q=0.8"
, 'sec-ch-ua': "\"Chromium\";v=\"112\", \"Google Chrome\";v=\"112\", \"Not:A-Brand\";v=\"99\""
, "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36"
, "User-Agent": browser_string
};


Expand All @@ -236,19 +238,22 @@ function carelinkSource (opts, axios) {
country: opts.countryCode
, lang: opts.languageCode
};
var headers = { };
var headers = {
'x-uniq-req': crypto.randomUUID()
, ...html_headers
};
console.log("AUTH WITH", modDefaults.login_url, params, headers);
return http.get(modDefaults.login_url, { params, headers }).then((resp) => {
console.log("FIRST STEP LOGIN FLOW", resp.headers, resp.data);
let regex = /(<form action=")(.*)" method="POST"/gm;
let endpoint = (regex.exec(resp.data) || [])[2] || '';
var regex = /(<form action=")(.*)" method="POST"/gm;
var endpoint = (regex.exec(resp.data) || [])[2] || '';

// Session data is changed, need to get it from the html body form
regex = /(<input type="hidden" name="sessionID" value=")(.*)"/gm;
let sessionID = (regex.exec(resp.data) || [])[2] || '';
var sessionID = (regex.exec(resp.data) || [])[2] || '';

regex = /(<input type="hidden" name="sessionData" value=")(.*)"/gm;
let sessionData = (regex.exec(resp.data)[2] || []) || '';
var sessionData = (regex.exec(resp.data)[2] || []) || '';
var loginFlow = {
endpoint
, sessionID
Expand All @@ -274,20 +279,24 @@ function carelinkSource (opts, axios) {
, actionButton: 'Log In'
};
delete payload.endpoint;
var headers = { 'content-type': 'application/x-www-form-urlencoded' };
var headers = {
'content-type': 'application/x-www-form-urlencoded'
, 'x-uniq-req': crypto.randomUUID( )
, ...html_headers
};
var params = { };
console.log("SUBMITTING LOGIN", loginFlow.endpoint, payload, params, headers);
return http.post(loginFlow.endpoint, qs.stringify(payload), { params, headers }).then((resp) => {
console.log("SUCCESS LOGGING IN", resp.headers, resp.data);
let regex = /(<form action=")(.*)" method="POST"/gm;
let endpoint = (regex.exec(resp.data) || [])[2] || '';
var regex = /(<form action=")(.*)" method="POST"/gm;
var endpoint = (regex.exec(resp.data) || [])[2] || '';

// Session data is changed, need to get it from the html body form
regex = /(<input type="hidden" name="sessionID" value=")(.*)"/gm;
let sessionID = (regex.exec(resp.data) || [])[2] || '';
var sessionID = (regex.exec(resp.data) || [])[2] || '';

regex = /(<input type="hidden" name="sessionData" value=")(.*)"/gm;
let sessionData = (regex.exec(resp.data)[2] || []) || '';
var sessionData = (regex.exec(resp.data)[2] || []) || '';
var cookies = (resp.headers['set-cookie'] || []).map(tough.parse);
if (cookies.length < 1) {
var err = new Error("Invalid Medtronic Carelink Credentials.");
Expand Down Expand Up @@ -323,6 +332,8 @@ function carelinkSource (opts, axios) {
var params = { };
var headers = {
'content-type': 'application/x-www-form-urlencoded'
, 'x-uniq-req': crypto.randomUUID( )
, ...html_headers
// , 'Cookie': flow.cookies.map((c) => { return new tough.Cookie(c).cookieString( ); })
};
console.log("SUBMITTING CONSENT FLOW", flow.endpoint, payload, params, headers);
Expand Down Expand Up @@ -375,6 +386,10 @@ function carelinkSource (opts, axios) {
headers = authed_headers;
}
function getUser ( ) {
var headers = {
...authed_headers
, 'x-uniq-req': crypto.randomUUID( )
};
return http.get(modDefaults.me_url, { headers }).then((resp) => {
console.log("SUCCESS FETCHING USER", resp.headers, resp);
account.user = resp.data;
Expand All @@ -390,7 +405,11 @@ function carelinkSource (opts, axios) {
}

function getProfile ( ) {
return http.get(modDefaults.my_profile_url, { headers: authed_headers }).then((resp) => {
var headers = {
...authed_headers
, 'x-uniq-req': crypto.randomUUID( )
};
return http.get(modDefaults.my_profile_url, { headers }).then((resp) => {
console.log("GOT PROFILE", resp.headers, resp.data);
account.profile = resp.data;
return resp.data;
Expand All @@ -404,7 +423,11 @@ function carelinkSource (opts, axios) {
countryCode: opts.countryCode
, language: opts.languageCode
};
return http.get(modDefaults.country_settings_url, { params, headers: authed_headers }).then((resp) => {
var headers = {
...authed_headers
, 'x-uniq-req': crypto.randomUUID( )
};
return http.get(modDefaults.country_settings_url, { params, headers }).then((resp) => {
console.log("GOT COUNTRY SETTINGS", resp.headers, resp.data);
account.requirements = resp.data;
return resp.data;
Expand All @@ -414,7 +437,11 @@ function carelinkSource (opts, axios) {
}

function getM2M ( ) {
return http.get(modDefaults.config_check_url, { headers: authed_headers }).then((resp) => {
var headers = {
...authed_headers
, 'x-uniq-req': crypto.randomUUID( )
};
return http.get(modDefaults.config_check_url, { headers }).then((resp) => {
console.log("GOT M2M SETTINGS", resp.headers, resp.data);
account.m2m_enabled = resp.data.value;
return resp.data;
Expand All @@ -433,7 +460,11 @@ function carelinkSource (opts, axios) {

return false;
}
return http.get(modDefaults.patient_list_url, { headers: authed_headers }).then((resp) => {
var headers = {
...authed_headers
, 'x-uniq-req': crypto.randomUUID( )
};
return http.get(modDefaults.patient_list_url, { headers }).then((resp) => {
console.log("PATIENT LIST", resp.data);
account.patient_list = resp.data;
return resp.data;
Expand Down Expand Up @@ -477,6 +508,10 @@ function carelinkSource (opts, axios) {
, msgType: 'last24hours'
, requestTime: Date.now( )
};
var headers = {
...authed_headers
, 'x-uniq-req': crypto.randomUUID( )
};
if (!session.patientUsername) {
return Promise.resolve(new Error("no patientUsername"));
}
Expand All @@ -496,12 +531,16 @@ function carelinkSource (opts, axios) {
username: session.profile.username
, role: "patient"
};
var headers = {
...authed_headers
, 'x-uniq-req': crypto.randomUUID( )
};
if (!session.isPatient) {
body.role = "carepartner";
body.patientId = session.patientUsername;
}
// if (session.isPatient) { return Promise.resolve( ); }
return http.post(session.requirements.blePereodicDataEndpoint, body, { headers: authed_headers }).then((resp) => {
return http.post(session.requirements.blePereodicDataEndpoint, body, { headers }).then((resp) => {
console.log("BLE PERIODIC ENDPOINT DATA", resp.headers, resp.data);
return resp.data;
}).catch((error) => {
Expand All @@ -512,7 +551,11 @@ function carelinkSource (opts, axios) {
function getMonitorData ( ) {
// TODO: obsoleted by M2M?
// deviceFamily determines whether to use M2M or BLE endpoint.
return http.get(modDefaults.monitor_data_url, { headers: authed_headers }).then((resp) => {
var headers = {
...authed_headers
, 'x-uniq-req': crypto.randomUUID( )
};
return http.get(modDefaults.monitor_data_url, { headers }).then((resp) => {

console.log("MONITOR DATA ENDPOINT DATA", resp.headers, resp.data);
return resp.data;
Expand All @@ -525,7 +568,11 @@ function carelinkSource (opts, axios) {
var params = {
numUploads: 1
};
return http.get(modDefaults.recent_uploads_url, { params, headers: authed_headers }).then((resp) => {
var headers = {
...authed_headers
, 'x-uniq-req': crypto.randomUUID( )
};
return http.get(modDefaults.recent_uploads_url, { params, headers }).then((resp) => {
console.log("RECENT UPLOADS DATA", resp.headers, resp.data);
return resp.data;
}).catch((error) => {
Expand Down Expand Up @@ -561,7 +608,10 @@ function carelinkSource (opts, axios) {
var authed_headers = {
Authorization: `Bearer ${session.token}`
};
var headers = authed_headers;
var headers = {
...authed_headers
, 'x-uniq-req': crypto.randomUUID( )
};
http.post(modDefaults.refresh_token_url, '', { headers }).then((resp) => {
console.log("SUCCESS WITH REFRESH FOR", resp.headers, resp.data);
var cookies = jar.getCookiesSync(baseURL);
Expand Down

0 comments on commit 94f6436

Please sign in to comment.