Skip to content

Instantly share code, notes, and snippets.

@trafficonese
Forked from srvanderplas/jqfp.js
Created October 24, 2018 14:12
Show Gist options
  • Save trafficonese/587f99d281d0e1096c4b19fa9eb7c465 to your computer and use it in GitHub Desktop.
Save trafficonese/587f99d281d0e1096c4b19fa9eb7c465 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")
)
))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment