Http request in EOS to control theHandy

Post all technical issues and questions here. We'll gladly help you wherever we can.
Post Reply
notSafeForDev
Explorer
Explorer
Posts: 33
Joined: Sun Sep 06, 2020 12:39 pm

Http request in EOS to control theHandy

Post by notSafeForDev »

Hi,

Now that EOS is working again, I thought I would see if it's possible to make a tease able to control theHandy, (It's an automatic sex toy similar to fleshlight launch). However, it turns out that XMLHttpRequest is not accessible in EOS so you can't send requests to theHandy API. So it would be great if making http requests were supported.

Though if there are potential security issues with that, one idea would be to have an action for stroking to a metronome. This action would also be able control any compatible sex toys, so users can create teases that works with and without a sex toy, without having to add anything extra. Which also means that support for any future toys could be added, and have them work with previously released teases.
fapnip
Explorer At Heart
Explorer At Heart
Posts: 430
Joined: Mon Apr 06, 2020 1:54 pm

Re: Http request in EOS to control theHandy

Post by fapnip »

EOS strictly limits teases from doing these kinds of things, mainly to protect privacy.

So, unfortunately, and fortunately, no. You can't do that in EOS, at least not without a custom Chrome extension or monkey script to inject such functionality into EOS.
notSafeForDev
Explorer
Explorer
Posts: 33
Joined: Sun Sep 06, 2020 12:39 pm

Re: Http request in EOS to control theHandy

Post by notSafeForDev »

fapnip wrote: Tue Oct 13, 2020 4:49 pm EOS strictly limits teases from doing these kinds of things, mainly to protect privacy.

So, unfortunately, and fortunately, no. You can't do that in EOS, at least not without a custom Chrome extension or monkey script to inject such functionality into EOS.
Ah, that's a good point. Though if there were a set of whitelisted domains then privacy shouldn't be an issue.

I have considered making a chrome extension or a tamper monkey script, but it would be tricky to account for all the different phrases for stroking. So something built into EOS would be ideal.
fapnip
Explorer At Heart
Explorer At Heart
Posts: 430
Joined: Mon Apr 06, 2020 1:54 pm

Re: Http request in EOS to control theHandy

Post by fapnip »

notSafeForDev wrote: Tue Oct 13, 2020 5:19 pm I have considered making a chrome extension or a tamper monkey script, but it would be tricky to account for all the different phrases for stroking. So something built into EOS would be ideal.
A monkey script should be able to tamper with the js for one of the existing EOS modules as the browser downloads the script, making it appear as though your additions are built in to EOS. If you injected it into, say, the PageManager module, you could probably add some method that's addressed by something like pages.handyApi(...) that can handle the necessary XMLHttpRequests outside the JS Interpreter sandbox. You could then add an if(!pages.handyApi) {pages.goto('tell-user-how-to-install-monkey-script-and-why-they-might-not-want-to-page')}

Giving EOS access to any teledildonic API will always be a security/privacy issue. I very much doubt such a feature would ever be added to EOS natively.

I'm guessing there's got to be some way to control the handy via audio?
fapnip
Explorer At Heart
Explorer At Heart
Posts: 430
Joined: Mon Apr 06, 2020 1:54 pm

Re: Http request in EOS to control theHandy

Post by fapnip »

Hopefully this isn't against any Milovana rules, but here's an example userscript (Only tested with Violentmonkey extension) that will inject an EOS module:

I'm a little hesitant to release this in fear of the privacy and security pandora's box I've just opened. Needless to say, people should never install userscripts from untrusted sources!

That said, the Content Security Policy for eosscript.com is locked down pretty well, so it's a pain to get any XMLHttpRequest outside of eosscript.com. I resorted to using the parent window to send XMLHttpRequest via postMessage. It's a bit convoluted, but it kinda works. Also, EOS doesn't handle the asynchronous mode of the jsInterpreter very well, so it's a pain to get results back from an XMLHttpRequest.

Code: Select all

// ==UserScript==
// @name        EOS Module Injector Test
// @namespace   https://eosscript.com/eos_module_injector_test
// @description EOS Module Injector Test
// @version     1.2
// @include     https://eosscript.com/*
// @include     https://milovana.com/webteases/*
// @include     https://milovana.com/eos/*
// @grant       GM_addScript
// @grant       GM_xmlhttpRequest
// @run-at      document-idle
// ==/UserScript==

/***************************
 * READ ME:
 * Make sure you change @name, @namespace, @description and var name to something unique to you!
 * 
 * Make sure you keep your module names namespace unique as well.  Don't use 'injectorTest'!!!
 * 
 */

// Module specific code.  Just about everything here should be changed for your specific use case!

var name = 'EOS Module Injector Test';

var DEBUG = true;

var ipAddressLastResult = false;

var modules = {
  injectorTest: {
    queryIpAddress: function (val) {
      // Dump what we got
      console.warn('Injector Test Got Value:', val, arguments);
      ipAddressLastResult = false; // clear our last result
      // Return something we shouldn't have access to in EOS as a test
      // Since it's a XMLHttpRequest, we'll need to run it in our parent window's scope:
      runParentAction('queryIpAddress'); // Send signal to parent action requesting client ip address
      // Note: we'll get an "Unknown rpc method" error on the console.  No way around this, but it's benign.
    },
    getLastIpQuery: function (val) {
      return ipAddressLastResult;
    },
  },
  // More modules ...
}

// Actions that will run on child (eosscripts.com, iframe)
var childActions = {
  returnIpAddress: function (ip) {
    console.log('Got client IP address', ip);
    ipAddressLastResult = ip;
    // Send signal to EOS tease that we got an updated IP address
    dispatchEvent('injectorTest', 'ip');
  },
  // More childActions ...
}

// Actions that will run on parent (milovana.com, parent window)
var parentActions = {
  queryIpAddress: function () {
    // Use parent window to run XMLHttpRequest, since our iframe doesn't have access
    var xhr = new XMLHttpRequest();
    xhr.open("GET", 'https://www.cloudflare.com/cdn-cgi/trace'); 
    xhr.onreadystatechange = function () {
      // In local files, status is 0 upon success in Mozilla Firefox
      if(xhr.readyState === XMLHttpRequest.DONE) {
        var status = xhr.status;
        var result = false;
        if (status === 0 || (status >= 200 && status < 400)) {
          // The request has been completed successfully
          result = (xhr.responseText || '').match(/[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/ );
          if (result) result = result[0];
          console.log('queryIpAddress result', xhr, xhr.responseText);
        } else {
          console.error('queryIpAddress error', xhr);
          // Oh no! There has been an error with the request!
          result = 'unable to obtain';
        }
        // Send result to EOS iframe
        runChildAction('returnIpAddress', result);
      }
    };
    xhr.send();
  },
  // More parentActions ...
}



// Common Module Injector Operations
// Shouldn't need to change anything below here

if (DEBUG) console.warn('Installing ' + name);

var interpreter;
var host;
var hooked = false;
var protos = {};

if (window.location.host.match(/eosscript\.com/)) {
  // EOS Script site
  // Hook console.info as a trigger for when Interpreter prototype is ready.
  // TODO:  find a better way to detect that 
  var origInfo = console.info;
  var origInterpreterRun;
  console.info = function () {
    var a = arguments[0];
    if (DEBUG) console.log('Hooking console.info', arguments);
    if (!hooked && typeof a === 'string' && a.match(/^loaded module:/)) {
      console.log('Interpreter prototype', Interpreter, window.Interpreter, window.Interpreter.prototype);
      origInterpreterRun = window.Interpreter.prototype.run;
      // Hook JS Interpreter's run method
      window.Interpreter.prototype.run = function () {
        // Wait until PageManager property exists so we know EOS has added its methods
        if (!this['_RUN_START_'+name] && this.globalObject.properties.PageManager) {
          // Now inject our methods into the Interpreter
          this['_RUN_START_'+name] = true; // And make sure we don't do it again.
          if (DEBUG) console.log('Intercepting Interpereter Run', this, arguments);
          console.log('Installing ' + name + ' modules...');
          interpreter = this;
          addObjectToInterpreter(interpreter.globalObject, modules);
        }
        return origInterpreterRun.apply(this, arguments);
      } 
      hooked = true; // Don't restore original hook, or we will break other EOS Injectors
    }
    return origInfo.apply(console, arguments);
  }
  window.addEventListener("message", function (e) {
    if (e.data && e.data.source === name && childActions[e.data.action]) {
      if (DEBUG) console.log('Running child action', e.data.action, e.data);
      childActions[e.data.action].apply(this, e.data.values || [])
    }
  }, false);
} else {
  // Milovana.com?
  window.addEventListener("message", function (e) {
    if (e.data && e.data.source === name && parentActions[e.data.action]) {
      if (DEBUG) console.log('Running parent action', e.data.action, e.data);
      parentActions[e.data.action].apply(this, e.data.values || [])
    }
  }, false);
}

function dispatchEvent(target, type) {
  if (!host) {
      host = document.getElementById('eosContainer')._reactRootContainer._internalRoot.current.child.stateNode.props.host
  }
  if (!protos[target]) {
    console.error('Unable to dispatchEvent.  No known proto:', target);
    return;
  }
  host.virtualMachine.dispatchEvent({ target: protos[target], type: type });
}

// Add defined modules / properties / functions to interpereter
function addObjectToInterpreter (base, obj) {
  for (var i in obj) {
    if (base[i] !== undefined || (base.properties && base.properties[i] !== undefined)) {
      console.error('Property `'+i+'` already exists in object.  Unable to add 3rd party module/property', i, base, base.properties);
      continue;
    }
    var el = obj[i];
    if (typeof el === 'object') {
      var protoName = i.charAt(0).toUpperCase() + i.slice(1);
      if (base[protoName] !== undefined || (base.properties && base.properties[protoName] !== undefined)) {
        console.error('Prototype `'+protoName+'` already exists in object.  Unable to add 3rd party module/property', i, protoName, base, base.properties);
        continue;
      }
      // var container = interpreter.nativeToPseudo({});
      // interpreter.setProperty(base, i, container);
      var constructor = function () {
        throw new Error('Cannot construct ' + protoName + ' object, use `' + i + '` global')
      }
      
      var constructorf = interpreter.createNativeFunction(constructor, true)
      interpreter.setProperty(
        constructorf,
        'prototype',
        interpreter.createObject(interpreter.globalObject.properties['EventTarget']),
        Interpreter.NONENUMERABLE_DESCRIPTOR
      )
      
      addObjectToInterpreter(constructorf, el);
      
      var protof = constructorf.properties['prototype'];
      interpreter.setProperty(interpreter.globalObject, protoName, constructorf);
      var proto = interpreter.createObjectProto(protof);
      protos[protoName] = proto;
      protos[i] = proto;
      
      interpreter.setProperty(base, i, proto);
      
      if (base === interpreter.globalObject) console.log('Loaded 3rd party module:', protoName);
      
    } else if (typeof el === 'function') {
      if (base === interpreter.globalObject) {
        interpreter.setProperty(base, i, interpreter.createNativeFunction(el));  
      } else {
        interpreter.setNativeFunctionPrototype(base, i, el);
      }
    } else {
      interpreter.setProperty(base, i, el);
    }
  }
}

function runChildAction(action) {
  if (DEBUG) console.log('Requesting child action', action, arguments);
  if (!childActions[action]) {
    console.error('No child action defined for:', action);
    return;
  }
  var values = [];
  for (var i = 1, l = arguments.length; i < l; i++) {
    values.push(arguments[i]);
  }
  var iframes = document.getElementsByClassName('eosIframe');
  var message = {
    source: name, 
    action: action,
    values: values
  };
  for (var i = 0, l = iframes.length; i < l; i++) {
    iframes[i].contentWindow.postMessage(message, '*');
  }
}

function runParentAction(action) {
  if (DEBUG) console.log('Requesting parent action', action, arguments);
  if (!parentActions[action]) {
    console.error('No parent action defined for:', action);
    return;
  }
  var values = [];
  for (i = 1, l = arguments.length; i < l; i++) {
    values.push(arguments[i]);
  }
  window.parent.postMessage({
    source: name, 
    action: action,
    values: values
  }, '*');
}
Zipped version of script:
eos_injector_test.user.js.zip
(3 KiB) Downloaded 73 times
And here's an EOS tease that uses it:
https://milovana.com/webteases/showteas ... 76d5c35028
And the JSON for it:
https://milovana.com/webteases/geteossc ... 76d5c35028
Last edited by fapnip on Thu Oct 15, 2020 5:55 pm, edited 11 times in total.
notSafeForDev
Explorer
Explorer
Posts: 33
Joined: Sun Sep 06, 2020 12:39 pm

Re: Http request in EOS to control theHandy

Post by notSafeForDev »

fapnip wrote: Tue Oct 13, 2020 10:33 pm Hopefully this isn't against any Milovana rules, but here's an example userscript (Only tested with Violentmonkey extension) that will inject an EOS module:

I'm a little hesitant to release this in fear of the privacy and security pandora's box I've just opened. Needless to say, people should never install userscripts from untrusted sources!

That said, the Content Security Policy for eosscript.com is locked down pretty well, so it's a pain to get any XMLHttpRequest outside of eosscript.com. I resorted to using the parent window to send XMLHttpRequest via postMessage. It's a bit convoluted, but it kinda works. Also, EOS doesn't handle the asynchronous mode of the jsInterpreter very well, so it's a pain to get results back from an XMLHttpRequest.

Code: Select all

// ==UserScript==
// @name        EOS Module Injector Test
// @namespace   https://eosscript.com/eos_module_injector_test
// @description EOS Module Injector Test
// @include     https://eosscript.com/*
// @include     https://milovana.com/webteases/*
// @include     https://milovana.com/eos/*
// @version     0.3
// @grant       GM_addScript
// @grant       GM_xmlhttpRequest
// @run-at      document-idle
// ==/UserScript==

var name = 'EOS Module Injector Test';

var injectorTestcallbackStack  = [];
var injectorTestLastResult = 'Unknown';

var modules = {
  injectorTest: function (val, callback) {
    // Dump what we got
    console.warn('Injector Test Got Value:', val);
    // Return something we shouldn't have access to in EOS
    if (!injectorTestcallbackStack.length) {
      // We're not processing any current calls.  Start a new one.
      injectorTestcallbackStack.push({
        callback: callback, // EOS doesn't work with asynchronous scripting, so we'll need to cheat anoter way
      });
      window.parent.postMessage({caller: 'injectorTest'}, '*');
      callback(injectorTestLastResult); // Return our last result
    } else {
      callback(injectorTestLastResult); // Return our last result
    }
  }
}

var moduleClientListeners = {
  injectorTest: function (e) {
    console.log('Iframe Got Message', e);
    if (e.data && e.data.caller === 'injectorTest') {
      var cb = injectorTestcallbackStack.pop();
      if (cb) {
        // cb.callback(e.data.result);
        injectorTestLastResult = e.data.result; 
      } else {
        console.error('No CB entry for message:',e);
      }
    }
  }
}

var moduleParentListeners = {
  injectorTest: function (e) {
    console.log('Parent Got Message', e);
    if (e.data && e.data.caller === 'injectorTest') {
      // Use parent window to run XMLHttpRequest, since our iframe doesn't have access
      var xhr = new XMLHttpRequest();
      xhr.open("GET", 'https://www.cloudflare.com/cdn-cgi/trace'); 
      xhr.onreadystatechange = function () {
        // In local files, status is 0 upon success in Mozilla Firefox
        if(xhr.readyState === XMLHttpRequest.DONE) {
          var status = xhr.status;
          var result;
          if (status === 0 || (status >= 200 && status < 400)) {
            // The request has been completed successfully
            var result = (xhr.responseText || '').match(/[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/ )
            console.log('xhr result', xhr, result);
          } else {
            console.log('xhr error');
            // Oh no! There has been an error with the request!
          }
          if (result) {
            result = 'Your IP Address is: ' + result[0];
          } else {
            result = 'Unable to determine your IP address';
          }
          var iframes = document.getElementsByClassName('eosIframe');
          for (var i = 0, l = iframes.length; i < l; i++) {
            var iframe = iframes[i];
            iframe.contentWindow.postMessage({
              caller: 'injectorTest',
              result: result
            }, '*');
          }
        }
      };
      xhr.send();
    }
  }
}

console.warn('Installing ' + name);

var jsInterpreter;

if (window.location.host.match(/eosscript\.com/)) {
  // EOS Script site
  // Hook console.info as a trigger when to start looking for JS Interpreter
  var origInfo = console.info;
  console.info = function () {
    var a = arguments[0]
    console.log('Hooking console.info', arguments);
    if (typeof a === 'string' && a.match(/^loaded module:/)) {
      console.warn('Waiting for JS Interpreter');
      waitForJsInterpreter(); 
      console.info = origInfo;
    }
    return origInfo.apply(console, arguments);
  }
  for (var i in moduleClientListeners) {
    window.addEventListener("message", moduleClientListeners[i], false);
  }
} else {
  // Milovana.com?
  for (var i in moduleParentListeners) {
    window.addEventListener("message", moduleParentListeners[i], false);
  }
}


// Ugly hack to keep looking for EOS's JSInterpreter instance until it's created.
// (There's probably an event we could listen for instead.)
function waitForJsInterpreter() {
  try {
    jsInterpreter = document.getElementById('eosContainer')._reactRootContainer._internalRoot.current.child.stateNode.props.host.virtualMachine._interpreter
  } catch (e) {
  }
  if (!jsInterpreter) {
    setTimeout(waitForJsInterpreter, 0);
  } else {
    for (var i in modules) {
      jsInterpreter.setProperty(jsInterpreter.globalObject, i, jsInterpreter.createAsyncFunction(modules[i]));
      console.log('Loaded ' + name + ' module:', i)
    }
  }
}
And here's an EOS tease that uses it:
https://milovana.com/webteases/showteas ... 76d5c35028
And the JSON for it:
https://milovana.com/webteases/geteossc ... 76d5c35028
Thanks for sharing this!

While privacy is a valid concern, I feel that the flexibility this gives is worth it.

I'll have to play around with this a bit later.
fapnip
Explorer At Heart
Explorer At Heart
Posts: 430
Joined: Mon Apr 06, 2020 1:54 pm

Re: Http request in EOS to control theHandy

Post by fapnip »

notSafeForDev wrote: Wed Oct 14, 2020 2:47 pm Thanks for sharing this!

While privacy is a valid concern, I feel that the flexibility this gives is worth it.

I'll have to play around with this a bit later.
I've updated the userscript and test tease a bit. Make sure you're using the newest. (v1.0)
notSafeForDev
Explorer
Explorer
Posts: 33
Joined: Sun Sep 06, 2020 12:39 pm

Re: Http request in EOS to control theHandy

Post by notSafeForDev »

fapnip wrote: Wed Oct 14, 2020 3:07 pm
notSafeForDev wrote: Wed Oct 14, 2020 2:47 pm Thanks for sharing this!

While privacy is a valid concern, I feel that the flexibility this gives is worth it.

I'll have to play around with this a bit later.
I've updated the userscript and test tease a bit. Make sure you're using the newest. (v1.0)
The code you have written have helped a lot for getting started.

This is my code so far for making requests to theHandy API:

Code: Select all

// ==UserScript==
// @name        theHandy EOS Support
// @namespace   Violentmonkey Scripts
// @match       *://*/*
// @grant       GM_addScript
// @grant       GM_xmlhttpRequest
// @version     0.1
// @author      notSafeForDev
// @description Adds support for theHandy in Milovana EOS teases. Credits to fapnip at Milovana.com for help with creating the script.
// @include     https://eosscript.com/*
// @include     https://milovana.com/webteases/*
// @include     https://milovana.com/eos/*
// ==/UserScript==

const isEOSIFrame = window.location.host.match(/eosscript\.com/) !== null;
const eosIFrame = document.getElementsByClassName('eosIframe')[0];
let interpreter = undefined;

// A function has to include at least one argument, 
// with the last argument being a callback for when the function is done
const customEOSFunctions = {
  setModeOff: (key, callback) => {
    postMessageToParent({command: "set mode off", key: key});
    callback();
  },
  setModeAutomatic: (key, callback) => {
    postMessageToParent({command: "set mode automatic", key: key});
    callback();
  },
  setSpeed: (key, value, callback) => {
    postMessageToParent({command: "set speed", key: key, value: value});
    callback();
  }
}

if (isEOSIFrame === true) {
  getJSInterpreter((obj) => {
    interpreter = obj;
    
    interpreter.setProperty(interpreter.globalObject, "lastHandyAPIResponse", interpreter.nativeToPseudo({}));
    
    for (let key in customEOSFunctions) {
      interpreter.setProperty(interpreter.globalObject, key, interpreter.createAsyncFunction(customEOSFunctions[key]));
      console.log("Loaded " + name + " module: " + key);
    }
  });
  
  window.addEventListener("message", onEOSIFrameMessage, false);
} else {
  window.addEventListener("message", onParentMessage, false);
}

function getJSInterpreter(callback) {
  let obj = undefined;
  try {
    obj = document.getElementById("eosContainer")._reactRootContainer._internalRoot.current.child.stateNode.props.host.virtualMachine._interpreter;
  } catch (e) {
  }

  if (obj !== undefined) {
    callback(obj);
  } else {
    requestAnimationFrame(getJSInterpreter.bind(this, callback));
  }
}

function postMessageToParent(data) {
  window.parent.postMessage(data, "*");
}

function postMessageToIFrame(data) {
  if (eosIFrame !== undefined) {
    eosIFrame.contentWindow.postMessage(data, "*");
  }
}

function onEOSIFrameMessage(e) {
  if (e.data.command === "set last handy api response") {
    interpreter.setProperty(interpreter.globalObject, "lastHandyAPIResponse", interpreter.nativeToPseudo(e.data.value));
  }
}

function onParentMessage(e) {  
  if (e.data.command === "set mode off") {
    makeRequest(getAPIUrl() + e.data.key + "/setMode?mode=0&timeout=5000", (response) => {
      postMessageToIFrame({command: "set last handy api response", value: response});
    });
  }
  
  if (e.data.command === "set mode automatic") {
    makeRequest(getAPIUrl() + e.data.key + "/setMode?mode=1&timeout=5000", (response) => {
      postMessageToIFrame({command: "set last handy api response", value: response});
    });
  }
  
  if (e.data.command === "set speed") {
    makeRequest(getAPIUrl() + e.data.key + "/setSpeed?timeout=5000&type=%25&speed=" + e.data.value, (response) => {
      postMessageToIFrame({command: "set last handy api response", value: response});
    });
  }
}

function getAPIUrl(key) {
  return "https://www.handyfeeling.com/api/v1/";
}

function makeRequest(url, onResponse) {
  const request = new XMLHttpRequest();
  request.open("GET", url);
  request.send();

  request.onreadystatechange = () => {
    if (request.readyState === 4 && onResponse !== undefined) {
      onResponse(JSON.parse(request.responseText));
    }
  }
}
In EOS you can check if a request was successful with lastHandyAPIResponse.error.
fapnip
Explorer At Heart
Explorer At Heart
Posts: 430
Joined: Mon Apr 06, 2020 1:54 pm

Re: Http request in EOS to control theHandy

Post by fapnip »

I'd advise sticking those new EOS functions in an object to keep the global namespace clean, or at the very least making them more unique to avoid any conflicts if others start creating more EOS userscripts.

If you look at my v1.1 userscript, you'll see I updated it to be a bit less brute force on how it gets the interpreter instance, and also now injects before the main EOS script starts, allowing the methods to be used by the init script on its initial load. (The hooks are still ugly, but it works, and is better than constant polling via setTImeout or requestAnimationFrame. But it does require at least one EOS module (audio, notifications, etc.) to be enabled since I use the console.info output from EOS to detect when the Interpreter prototype is ready to hook.)

Also made it easier to keep the interpereter global namespace a little cleaner.

And also added event dispatch support, so you don't need to poll for data changes in EOS.

Sorry, It's all ES5, but that's just the grove I'm in when dealing with anything EOS.
Post Reply

Who is online

Users browsing this forum: No registered users and 5 guests