Skip to content

Instantly share code, notes, and snippets.

@srvanderplas
Last active June 17, 2023 04:58
Show Gist options
  • Save srvanderplas/6049567 to your computer and use it in GitHub Desktop.
Save srvanderplas/6049567 to your computer and use it in GitHub Desktop.
Shiny user fingerprint (md5 hash of browser characteristics) and ip address demo. The .R files are in the working directory ("./"), the .js files must be placed in ./www/js/ to be accessible. I wrote the R code, which I will release under the same WTF license as the jqfp.js library is released under.
// Browser fingerprinting is a technique to "mark" anonymous users using JS
// (or other things). To build an "identity" of sorts the browser is queried
// for a list of its plugins, the screen size and several other things, then
// hashes them. The idea is that these bits of information produce an unique
// "fingerprint" of sorts; the more elaborate the list of data points is, the
// more unique this fingerprint becomes. And you wouldn't even need to set a
// cookie to recognize this user when she visits again.
//
// For more information on this topic consult
// [Ars Technica](http://arstechnica.com/tech-policy/news/2010/05/how-your-web-browser-rats-you-out-online.ars)
// or the [EFF](http://panopticlick.eff.org/). There is a lot of potential
// for undesirable shenanigans, and I strictly oppose using this technique for
// marketing and ad-related tracking purposes.
//
// Anyways, I needed a really simple fingerprinting library, so I wrote a
// quick and dirty jQuery plugin. This is by no means a complete and
// watertight implementation -- it is merely the scratch for a particular itch
// I was having. YMMV.
//
// This library was written by Carlo Zottmann, [email protected], has its home
// on [Github](http://github.com/carlo/jquery-browser-fingerprint) and is
// WTF-licensed (see LICENSE.txt).
( function($) {
// Calling `jQuery.fingerprint()` will return an MD5 hash, i.e. said
// fingerprint.
$.fingerprint = function() {
// This function, `_raw()`, uses several browser details which are
// available to JS here to build a string, namely...
//
// * the user agent
// * screen size
// * color depth
// * the timezone offset
// * sessionStorage support
// * localStorage support
// * the list of all installed plugins (we're using their names,
// descriptions, mime types and file name extensions here)
function _raw() {
// That string is the return value.
return [
navigator.userAgent,
[ screen.height, screen.width, screen.colorDepth ].join("x"),
( new Date() ).getTimezoneOffset(),
!!window.sessionStorage,
!!window.localStorage,
$.map( navigator.plugins, function(p) {
return [
p.name,
p.description,
$.map( p, function(mt) {
return [ mt.type, mt.suffixes ].join("~");
}).join(",")
].join("::");
}).join(";")
].join("###");
}
// `_md5()` computes a MD5 hash using [md5-js](http://github.com/wbond/md5-js/).
function _md5() {
if ( typeof window.md5 === "function" ) {
// The return value is the hashed fingerprint string.
return md5( _raw() );
}
else {
// If `window.md5()` isn't available, an error is thrown.
throw "md5 unavailable, please get it from http://github.com/wbond/md5-js/";
}
}
// And, since I'm lazy, calling `$.fingerprint()` will return the hash
// right away, without the need for any other calls.
return _md5();
}
})(jQuery);
/*!
* Joseph Myer's md5() algorithm wrapped in a self-invoked function to prevent
* global namespace polution, modified to hash unicode characters as UTF-8.
*
* Copyright 1999-2010, Joseph Myers, Paul Johnston, Greg Holt, Will Bond <[email protected]>
* http://www.myersdaily.org/joseph/javascript/md5-text.html
* http://pajhome.org.uk/crypt/md5
*
* Released under the BSD license
* http://www.opensource.org/licenses/bsd-license
*/
(function() {
function md5cycle(x, k) {
var a = x[0], b = x[1], c = x[2], d = x[3];
a = ff(a, b, c, d, k[0], 7, -680876936);
d = ff(d, a, b, c, k[1], 12, -389564586);
c = ff(c, d, a, b, k[2], 17, 606105819);
b = ff(b, c, d, a, k[3], 22, -1044525330);
a = ff(a, b, c, d, k[4], 7, -176418897);
d = ff(d, a, b, c, k[5], 12, 1200080426);
c = ff(c, d, a, b, k[6], 17, -1473231341);
b = ff(b, c, d, a, k[7], 22, -45705983);
a = ff(a, b, c, d, k[8], 7, 1770035416);
d = ff(d, a, b, c, k[9], 12, -1958414417);
c = ff(c, d, a, b, k[10], 17, -42063);
b = ff(b, c, d, a, k[11], 22, -1990404162);
a = ff(a, b, c, d, k[12], 7, 1804603682);
d = ff(d, a, b, c, k[13], 12, -40341101);
c = ff(c, d, a, b, k[14], 17, -1502002290);
b = ff(b, c, d, a, k[15], 22, 1236535329);
a = gg(a, b, c, d, k[1], 5, -165796510);
d = gg(d, a, b, c, k[6], 9, -1069501632);
c = gg(c, d, a, b, k[11], 14, 643717713);
b = gg(b, c, d, a, k[0], 20, -373897302);
a = gg(a, b, c, d, k[5], 5, -701558691);
d = gg(d, a, b, c, k[10], 9, 38016083);
c = gg(c, d, a, b, k[15], 14, -660478335);
b = gg(b, c, d, a, k[4], 20, -405537848);
a = gg(a, b, c, d, k[9], 5, 568446438);
d = gg(d, a, b, c, k[14], 9, -1019803690);
c = gg(c, d, a, b, k[3], 14, -187363961);
b = gg(b, c, d, a, k[8], 20, 1163531501);
a = gg(a, b, c, d, k[13], 5, -1444681467);
d = gg(d, a, b, c, k[2], 9, -51403784);
c = gg(c, d, a, b, k[7], 14, 1735328473);
b = gg(b, c, d, a, k[12], 20, -1926607734);
a = hh(a, b, c, d, k[5], 4, -378558);
d = hh(d, a, b, c, k[8], 11, -2022574463);
c = hh(c, d, a, b, k[11], 16, 1839030562);
b = hh(b, c, d, a, k[14], 23, -35309556);
a = hh(a, b, c, d, k[1], 4, -1530992060);
d = hh(d, a, b, c, k[4], 11, 1272893353);
c = hh(c, d, a, b, k[7], 16, -155497632);
b = hh(b, c, d, a, k[10], 23, -1094730640);
a = hh(a, b, c, d, k[13], 4, 681279174);
d = hh(d, a, b, c, k[0], 11, -358537222);
c = hh(c, d, a, b, k[3], 16, -722521979);
b = hh(b, c, d, a, k[6], 23, 76029189);
a = hh(a, b, c, d, k[9], 4, -640364487);
d = hh(d, a, b, c, k[12], 11, -421815835);
c = hh(c, d, a, b, k[15], 16, 530742520);
b = hh(b, c, d, a, k[2], 23, -995338651);
a = ii(a, b, c, d, k[0], 6, -198630844);
d = ii(d, a, b, c, k[7], 10, 1126891415);
c = ii(c, d, a, b, k[14], 15, -1416354905);
b = ii(b, c, d, a, k[5], 21, -57434055);
a = ii(a, b, c, d, k[12], 6, 1700485571);
d = ii(d, a, b, c, k[3], 10, -1894986606);
c = ii(c, d, a, b, k[10], 15, -1051523);
b = ii(b, c, d, a, k[1], 21, -2054922799);
a = ii(a, b, c, d, k[8], 6, 1873313359);
d = ii(d, a, b, c, k[15], 10, -30611744);
c = ii(c, d, a, b, k[6], 15, -1560198380);
b = ii(b, c, d, a, k[13], 21, 1309151649);
a = ii(a, b, c, d, k[4], 6, -145523070);
d = ii(d, a, b, c, k[11], 10, -1120210379);
c = ii(c, d, a, b, k[2], 15, 718787259);
b = ii(b, c, d, a, k[9], 21, -343485551);
x[0] = add32(a, x[0]);
x[1] = add32(b, x[1]);
x[2] = add32(c, x[2]);
x[3] = add32(d, x[3]);
}
function cmn(q, a, b, x, s, t) {
a = add32(add32(a, q), add32(x, t));
return add32((a << s) | (a >>> (32 - s)), b);
}
function ff(a, b, c, d, x, s, t) {
return cmn((b & c) | ((~b) & d), a, b, x, s, t);
}
function gg(a, b, c, d, x, s, t) {
return cmn((b & d) | (c & (~d)), a, b, x, s, t);
}
function hh(a, b, c, d, x, s, t) {
return cmn(b ^ c ^ d, a, b, x, s, t);
}
function ii(a, b, c, d, x, s, t) {
return cmn(c ^ (b | (~d)), a, b, x, s, t);
}
function md51(s) {
// Converts the string to UTF-8 "bytes" when necessary
if (/[\x80-\xFF]/.test(s)) {
s = unescape(encodeURI(s));
}
txt = '';
var n = s.length, state = [1732584193, -271733879, -1732584194, 271733878], i;
for (i = 64; i <= s.length; i += 64) {
md5cycle(state, md5blk(s.substring(i - 64, i)));
}
s = s.substring(i - 64);
var tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
for (i = 0; i < s.length; i++)
tail[i >> 2] |= s.charCodeAt(i) << ((i % 4) << 3);
tail[i >> 2] |= 0x80 << ((i % 4) << 3);
if (i > 55) {
md5cycle(state, tail);
for (i = 0; i < 16; i++) tail[i] = 0;
}
tail[14] = n * 8;
md5cycle(state, tail);
return state;
}
function md5blk(s) { /* I figured global was faster. */
var md5blks = [], i; /* Andy King said do it this way. */
for (i = 0; i < 64; i += 4) {
md5blks[i >> 2] = s.charCodeAt(i) +
(s.charCodeAt(i + 1) << 8) +
(s.charCodeAt(i + 2) << 16) +
(s.charCodeAt(i + 3) << 24);
}
return md5blks;
}
var hex_chr = '0123456789abcdef'.split('');
function rhex(n) {
var s = '', j = 0;
for (; j < 4; j++)
s += hex_chr[(n >> (j * 8 + 4)) & 0x0F] +
hex_chr[(n >> (j * 8)) & 0x0F];
return s;
}
function hex(x) {
for (var i = 0; i < x.length; i++)
x[i] = rhex(x[i]);
return x.join('');
}
md5 = function (s) {
return hex(md51(s));
}
/* this function is much faster, so if possible we use it. Some IEs are the
only ones I know of that need the idiotic second function, generated by an
if clause. */
function add32(a, b) {
return (a + b) & 0xFFFFFFFF;
}
if (md5('hello') != '5d41402abc4b2a76b9719d911017c592') {
function add32(x, y) {
var lsw = (x & 0xFFFF) + (y & 0xFFFF),
msw = (x >> 16) + (y >> 16) + (lsw >> 16);
return (msw << 16) | (lsw & 0xFFFF);
}
}
})();
library(shiny)
shinyServer(function(input, output, clientData) {
output$testtext <- renderText(paste(" fingerprint: ", input$fingerprint, " ip: ", input$ipid))
})
// Calling `jQuery.fingerprint()` will return an MD5 hash, i.e. said
// fingerprint.
$.fingerprint = function() {
// This function, `_raw()`, uses several browser details which are
// available to JS here to build a string, namely...
//
// * the user agent
// * screen size
// * color depth
// * the timezone offset
// * sessionStorage support
// * localStorage support
// * the list of all installed plugins (we're using their names,
// descriptions, mime types and file name extensions here)
function _raw() {
// That string is the return value.
return [
navigator.userAgent,getip(),
[ screen.height, screen.width, screen.colorDepth ].join("x"),
( new Date() ).getTimezoneOffset(),
!!window.sessionStorage,
!!window.localStorage,
$.map( navigator.plugins, function(p) {
return [
p.name,
p.description,
$.map( p, function(mt) {
return [ mt.type, mt.suffixes ].join("~");
}).join(",")
].join("::");
}).join(";")
].join("###");
}
// `_md5()` computes a MD5 hash using [md5-js](http://github.com/wbond/md5-js/).
function _md5() {
if ( typeof window.md5 === "function" ) {
// The return value is the hashed fingerprint string.
return md5( _raw() );
}
else {
// If `window.md5()` isn't available, an error is thrown.
throw "md5 unavailable, please get it from http://github.com/wbond/md5-js/";
}
}
// And, since I'm lazy, calling `$.fingerprint()` will return the hash
// right away, without the need for any other calls.
return _md5();
}
/*
var outputUserid = new Shiny.OutputBinding();
$.extend(outputUserid, {
find: function(scope) {
return $.find('.userid');
},
renderError: function(el,error) {
console.log("Foe");
},
renderValue: function(el,data) {
updateView(data);
console.log("Friend");
}
});
Shiny.outputBindings.register(outputUserid);
*/
var inputUseridBinding = new Shiny.InputBinding();
$.extend(inputUseridBinding, {
find: function(scope) {
return $.find('.userid');
},
getValue: function(el) {
return $(el).val();
},
setValue: function(el, values) {
$(el).attr("value", $.fingerprint());
$(el).trigger("change");
},
subscribe: function(el, callback) {
$(el).on("change.inputUseridBinding", function(e) {
callback();
});
},
unsubscribe: function(el) {
$(el).off(".inputUseridBinding");
}
});
Shiny.inputBindings.register(inputUseridBinding);
//setuid();
//A unique ID generated from the fingerprint of
// several browser characteristics.
shiny_uid=$.fingerprint();
/*
* Set the uid fingerprint into the DOM elements that need to know about it.
* Do not call before the form loads, or the selectors won't find anything.
*/
function setuid() {
var fph = $('.userid');
fph.attr("value", shiny_uid);
fph.trigger("change");
}
function setvalues(){
getip();
setuid();
}
/*
* Set the uid fingerprint into the DOM elements that need to know about it.
* Do not call before the form loads, or the selectors won't find anything.
*/
var inputIpBinding = new Shiny.InputBinding();
$.extend(inputIpBinding, {
find: function(scope) {
return $.find('.ipaddr');
},
getValue: function(el) {
return $(el).val();
},
setValue: function(el, values) {
$(el).attr("value", getip())
$(el).trigger("change");
},
subscribe: function(el, callback) {
$(el).on("change.inputIpBinding", function(e) {
callback();
});
},
unsubscribe: function(el) {
$(el).off(".inputIpBinding");
}
});
Shiny.inputBindings.register(inputIpBinding);
function getip() {
ip = null;
$.getJSON("http://jsonip.com?callback=?",
function(data){
ip = data.ip;
callback(ip);
$(".ipaddr").attr("value", ip);
$(".ipaddr").trigger("change");
//return ip address correctly
});
//alert(ip); //undefined or null
}
function callback(tempip)
{
ip=tempip;
// alert(ip); //undefined or null
}
library(shiny)
inputUserid <- function(inputId, value='') {
# print(paste(inputId, "=", value))
tagList(
singleton(tags$head(tags$script(src = "js/md5.js", type='text/javascript'))),
singleton(tags$head(tags$script(src = "js/shinyBindings.js", type='text/javascript'))),
tags$body(onload="setvalues()"),
tags$input(id = inputId, class = "userid", value=as.character(value), type="text", style="display:none;")
)
}
inputIp <- function(inputId, value=''){
tagList(
singleton(tags$head(tags$script(src = "js/md5.js", type='text/javascript'))),
singleton(tags$head(tags$script(src = "js/shinyBindings.js", type='text/javascript'))),
tags$body(onload="setvalues()"),
tags$input(id = inputId, class = "ipaddr", value=as.character(value), type="text", style="display:none;")
)
}
shinyUI(pageWithSidebar(
# Application title
headerPanel("FingerprintDemo"),
sidebarPanel(
inputIp("ipid"),
inputUserid("fingerprint"),
helpText("nothing to see here... hidden text elements aren't editable by user")),
mainPanel(
textOutput("testtext")
)
))
@jpd527
Copy link

jpd527 commented Oct 9, 2015

When trying this I had a conflict with the jsonip.com call because my Shiny Server uses SSL, so I just replaced the "http://jsonip.com?callback=?" with "https://api.ipify.org/?format=json" in shinyBindings.js and everything worked like a charm!

@oganm
Copy link

oganm commented Apr 13, 2018

"https://api.ipify.org/?format=json" seems dead but "https://jsonip.com?callback=?" works fine with bonus anti fascist quotes

@chuagh74
Copy link

chuagh74 commented Apr 5, 2019

When I use this with ui.R and server.R, I can only get fingerprint but not ip address.
When I combine the ui.R and server.R scripts into rmd to run in Shiny, I am not even able to get fingerprint. May I know why?

@angelobj
Copy link

"https://api.ipify.org/?format=json" seems dead but "https://jsonip.com?callback=?" works fine with bonus anti fascist quotes

I was having the same problem with the server, and your comment solved, thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment