1. #1
    Sencha User
    Join Date
    Feb 2012
    Posts
    16
    Answers
    1
    Vote Rating
    1
    Tequiol is on a distinguished road

      0  

    Default Answered: How can I build an extended proxy for Ajax requests?

    Answered: How can I build an extended proxy for Ajax requests?


    Hello,

    I'd like to build my own extension proxy for Ajax requests. I'll need this, because I want to add a transparent user authentication in a way, thats not just using username and password. The second problem is the way which is used be the application server to handle the method name.

    The application server uses JSON-RPC. The user authentication is done via HTTP-Auth. The username must be extended with a 32 char client id to provide application instance based row locking (so the username would be something like this <32 char hash>-<username>).

    I cannot find the right way to start. I tried to build an derivation of Ext.data.proxy.Ajax. The goal should be a proxy object, that can be used as a proxy for every store thats used in my application.

    This is working code I've used in a test application based on extjs 3. The new application should be written in extjs 4, using the store and model principal in the way it's designed:

    Code:
    Ext.define('Tequiol.util.JsonRpc', {
      username:'',
      password:'',
      clientId:'',
      authorizationRealm:'',
      timeout:30000,
    
      constructor:function (username, password, timeout) {
        this.username = username;
        this.password = password;
        this.timeout = timeout || 30000;
    
        this.clientId = Ext.create('Tequiol.util.Md5').checksum(Math.random());
        this.authorizationRealm = Ext.create('Tequiol.util.Base64').encode(this.clientId + '-' + this.username + ':' + this.password);
      },
    
      request:function (method, params, success) {
        Ext.Ajax.defaultHeaders = {'Authorization':'Basic ' + this.authorizationRealm};
        Ext.Ajax.timeout = this.timeout;
        Ext.Ajax.method = 'POST';
        Ext.Ajax.url = '/json/';
    
        Ext.Ajax.request({
          jsonData:{
            version:'1.1',
            method:method,
            params:params
          },
    
          success:function (result, request) {
            var decodedResponse = Ext.decode(result.responseText);
    
            if(decodedResponse.error) {
              Ext.MessageBox.alert('RPC-Error', 'Failed to call method ' + request.jsonData.method + '. Error: ' + decodedResponse.error.message + ' (Code: ' + decodedResponse.error.code + ')');
            }
            else {
              success(decodedResponse);
            }
          },
    
          failure:function (result, request) {
            Ext.MessageBox.alert('HTTP-Error', result.responseText);
          }
        });
      }
    });
    This works really nice, when triggered manually, but cannot be used as a proxy for data models.

    Can someone give me a starting point for writing a proxy? I'm not sure what to extend. A hint for the right class names and important functions to override would be enough. A link to an example would be great.

  2. Hello,

    hehe, I was just looking for something to call the inherited methods.

    Authentication is only needed (and wanted) when talking to the application server. There might be user extensions, that are using Ajax to get (for example) the current weather. So it's a good idea to have a own proxy class, which modifies the header. Isn't it the right way, to derive a class, instead of modifying it from the outside? The third party servers might be accessed via http proxy configured in the application server.

    I've updated my source code to use the buildRequest method instead of overriding doRequest. Here is my updated code:

    Code:
    Ext.define('Tequiol.store.JsonRpc', {
      requires: ['Tequiol.util.Md5', 'Tequiol.util.Base64'],
      extend:'Ext.data.proxy.Ajax',
      alias:'proxy.jsonrpc',
    
      reader:{
        type:'json',
        root:'result'
      },
    
      statics: {
        /**
         * @private
         * Stores authentication data.
         */
        authentication:'',
    
        /**
         * @public
         * Changes the authentication data and generates a new client id for the connection.
         * @param username to log in
         * @param password to log in
         */
        setAuth:function (username, password) {
          var clientId = Ext.create('Tequiol.util.Md5').checksum(Math.random());
          this.authentication = Ext.create('Tequiol.util.Base64').encode(clientId + '-' + username + ':' + password);
        },
    
        /**
         * @public
         * returns true, if authentication data is available (does not tell, if the user is logged in)
         */
        hasAuth:function () {
          return this.authentication != '';
        }
      },
    
      constructor:function (config) {
        config = config || {};
        this.url = config.url || '/json';
        this.method = config.method || '';
        this.params = config.params || {};
        this.callParent([config]);
      },
    
      /**
       * @private
       * Modified headers for authentication and adds JSON-RPC header to the data request.
       * @param operation
       */
      buildRequest:function(operation) {
        var request = this.callParent([operation]);
    
        this.headers = {
          'Authorization':'Basic ' + this.self.authentication,
          'Content-Type':'application/json; charset=UTF-8'
        };
    
        Ext.apply(request, {
          jsonData:{
            version:'1.1',
            method:this.method,
            params:this.params
          },
          params: {} // get rid of GET params
        });
    
        return request;
      },
    
      /**
       * @private
       * Method is always POST.
       * @param request
       */
      getMethod: function(request) {
        return 'POST';
      }
    });
    If you still have any suggestions, feel free to tell me.

  3. #2
    Sencha Premium Member skirtle's Avatar
    Join Date
    Oct 2010
    Location
    UK
    Posts
    3,508
    Answers
    528
    Vote Rating
    288
    skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future

      0  

    Default


    Your starting point should be something as simple as this:

    Code:
    Ext.define('MyProxy', {
        alias: 'proxy.myproxy',
        extend: 'Ext.data.proxy.Ajax'
    });
    The alias allows it to be used as a proxy type, so on your store's proxy config you'd write type: 'myproxy' instead of type: 'ajax'.

    To decide which method to override, start by browsing the docs, looking for suitable sounding method names:

    http://docs.sencha.com/ext-js/4-0/#!...ata.proxy.Ajax

    You might not find one that's perfect for what you want, all you're looking for is a starting point to jump into the code via the 'View Source' link so that you can start to better understand the class you're overriding.

    From a quick look myself, I think the method doRequest is worth a read:

    http://docs.sencha.com/ext-js/4-0/so...ata-proxy-Ajax

    Judging by the way it handles headers, I'd say that buildRequest gives you the override point you need.

    Alternatively, if you just want to add the header to every request from a particular point onwards, why not just add the header into Ext.Ajax.defaultHeaders at that point, rather than trying to do it for each individual request? That way you don't need to write custom wrappers for Ajax or proxies.

  4. #3
    Sencha User
    Join Date
    Feb 2012
    Posts
    16
    Answers
    1
    Vote Rating
    1
    Tequiol is on a distinguished road

      0  

    Default


    Hello,

    thanks for your assistance. I've produced a working proxy. Here is my source code:

    Code:
    Ext.define('Tequiol.store.JsonRpc', {
      requires: ['Tequiol.util.Md5', 'Tequiol.util.Base64'],
      extend:'Ext.data.proxy.Ajax',
      alias:'proxy.jsonrpc',
    
      reader:{
        type:'json',
        root:'result'
      },
    
      /**
       * @public
       * The url for POST request via JSON-Rpc
       */
      url:null,
    
      statics: {
        /**
         * @private
         * Stores authentication data.
         */
        authentication:'',
    
        /**
         * @public
         * Changes the authentication data and generates a new client id for the connection.
         * @param username to log in
         * @param password to log in
         */
        setAuth:function (username, password) {
          var clientId = Ext.create('Tequiol.util.Md5').checksum(Math.random());
          this.authentication = Ext.create('Tequiol.util.Base64').encode(clientId + '-' + username + ':' + password);
        },
    
        /**
         * @public
         * returns true, if authentication data is available (does not tell, if the user is logged in)
         */
        hasAuth:function () {
          return this.authentication != '';
        }
      },
    
      constructor:function (config) {
        config = config || {};
        this.callParent([config]);
        this.url = config.url || '/json';
        this.timeout = config.timeout || 30000;
        this.config = config;
      },
    
      doRequest:function (operation, callback, scope) {
        var writer = this.getWriter(),
          request = this.buildRequest(operation, callback, scope);
    
        if(operation.allowWrite()) {
          request = writer.write(request);
        }
    
        Ext.apply(request, {
          url:this.url,
          jsonData:{
            version:'1.1',
            method:this.config.method,
            params:this.config.params
          },
          headers:{
            'Authorization':'Basic ' + this.self.authentication,
            'Content-Type':'application/json; charset=UTF-8',
          },
          timeout:this.timeout,
          scope:this,
          callback:this.createRequestCallback(request, operation, callback, scope),
          method:'POST',
          disableCaching:false
        });
    
        Ext.Ajax.request(request);
    
        return request;
      }
    });
    Would be really nice to get a short code review.

    The authentication data is only cached in memory, when calling:

    Code:
    Tequiol.store.JsonRpc.setAuth(username, password);
    and is used for all connections after that.

    The proxy can be used in a data model like this:

    Code:
    Ext.define('Tequiol.model.GroupList', {
      extend:'Ext.data.Model',
      idProperty:'ID',
      fields:[
        'ID',
        'ParentRef',
        'Name'
      ],
      proxy: {
        type: 'jsonrpc',
        method: 'RpcGroups.GetList',
        params: [true]
      }
    });
    If you have any suggestions, please tell me.

  5. #4
    Sencha Premium Member skirtle's Avatar
    Join Date
    Oct 2010
    Location
    UK
    Posts
    3,508
    Answers
    528
    Vote Rating
    288
    skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future

      0  

    Default


    I don't understand why you've overridden doRequest and effectively reimplemented it. Surely you can achieve this with a small override of buildRequest instead?

    Given everything you've said, I still don't understand why you're creating you're own proxy. Why can't you just set the defaultHeaders once on Ext.Ajax and then everything works for free, including the standard Ajax proxy?

  6. #5
    Sencha User
    Join Date
    Feb 2012
    Posts
    16
    Answers
    1
    Vote Rating
    1
    Tequiol is on a distinguished road

      0  

    Default


    Hello,

    the problem (or might be I've misunderstood it) is, that the standard proxy have 4 methods for managing objects: list, add, edit and destroy. The application server has much more operations for an object. However, every operation thats not list, add oder destroy will be an edit in the background (just manipulation one property of the object or moving, cloning, etc). I think I'll have to manage that, when doing manipulations. Currently I don't know how to accomplish this. I've read the whole guide multiple times, but some concepts are still weird for me. There is lot of "magic" and abstraction, that makes things a little bit confusing at the beginning.

    This implementation is just a try to understand how it works. My implementation does the authentication part and adds the json-rpc header (version, method and params). But there is still something wrong, the request sends some GET params: ?query=&page=1&start=0&limit=25 - They are nonsense for JSON-RPC. How to remove them?

    However I think just adding some defaultHeaders to the global Ajax request is not the right way. For example this doesn't work, when deadling with multiple data sources via Ajax (using a server side proxy for other websites). But you're right, it might be possible to override buildRequest instead of doRequest. But I don't think, the things are getting shorter than. May be the "right" way is not to override Ext.data.proxy.Ajax but Ext.data.proxy.Server.

    Any suggestions are welcome.

  7. #6
    Sencha Premium Member skirtle's Avatar
    Join Date
    Oct 2010
    Location
    UK
    Posts
    3,508
    Answers
    528
    Vote Rating
    288
    skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future

      0  

    Default


    If you have some Ajax requests that don't require the authentication headers then you're correct, using defaultHeaders won't work. However, I'm not convinced by the use case you've described. If you're proxying another site then wouldn't you still want to authenticate in your proxy before forwarding on the requests (minus the authentication headers)?

    I still think subclassing Ajax proxy is the correct way to go, rather than Server proxy. You shouldn't be performing the Ajax request yourself, let the Ajax proxy do that bit. You just want to tweak the request settings, which you can do in a small override of buildRequest. Don't completely replace it, call the overridden method using callParent then make the small changes you need to the returned object.

    Those extra GET parameters are indeed sent by default. You can remove them using settings like startParam, see:

    http://docs.sencha.com/ext-js/4-0/#!...cfg-startParam

    There's one setting per parameter that you'll need to change.

  8. #7
    Sencha User
    Join Date
    Feb 2012
    Posts
    16
    Answers
    1
    Vote Rating
    1
    Tequiol is on a distinguished road

      0  

    Default


    Hello,

    hehe, I was just looking for something to call the inherited methods.

    Authentication is only needed (and wanted) when talking to the application server. There might be user extensions, that are using Ajax to get (for example) the current weather. So it's a good idea to have a own proxy class, which modifies the header. Isn't it the right way, to derive a class, instead of modifying it from the outside? The third party servers might be accessed via http proxy configured in the application server.

    I've updated my source code to use the buildRequest method instead of overriding doRequest. Here is my updated code:

    Code:
    Ext.define('Tequiol.store.JsonRpc', {
      requires: ['Tequiol.util.Md5', 'Tequiol.util.Base64'],
      extend:'Ext.data.proxy.Ajax',
      alias:'proxy.jsonrpc',
    
      reader:{
        type:'json',
        root:'result'
      },
    
      statics: {
        /**
         * @private
         * Stores authentication data.
         */
        authentication:'',
    
        /**
         * @public
         * Changes the authentication data and generates a new client id for the connection.
         * @param username to log in
         * @param password to log in
         */
        setAuth:function (username, password) {
          var clientId = Ext.create('Tequiol.util.Md5').checksum(Math.random());
          this.authentication = Ext.create('Tequiol.util.Base64').encode(clientId + '-' + username + ':' + password);
        },
    
        /**
         * @public
         * returns true, if authentication data is available (does not tell, if the user is logged in)
         */
        hasAuth:function () {
          return this.authentication != '';
        }
      },
    
      constructor:function (config) {
        config = config || {};
        this.url = config.url || '/json';
        this.method = config.method || '';
        this.params = config.params || {};
        this.callParent([config]);
      },
    
      /**
       * @private
       * Modified headers for authentication and adds JSON-RPC header to the data request.
       * @param operation
       */
      buildRequest:function(operation) {
        var request = this.callParent([operation]);
    
        this.headers = {
          'Authorization':'Basic ' + this.self.authentication,
          'Content-Type':'application/json; charset=UTF-8'
        };
    
        Ext.apply(request, {
          jsonData:{
            version:'1.1',
            method:this.method,
            params:this.params
          },
          params: {} // get rid of GET params
        });
    
        return request;
      },
    
      /**
       * @private
       * Method is always POST.
       * @param request
       */
      getMethod: function(request) {
        return 'POST';
      }
    });
    If you still have any suggestions, feel free to tell me.

  9. #8
    Sencha User Misiu's Avatar
    Join Date
    Jun 2012
    Location
    Poland
    Posts
    206
    Answers
    6
    Vote Rating
    33
    Misiu has a spectacular aura about Misiu has a spectacular aura about

      0  

    Default


    Tequiol could You please post code for util.Md5 and util.Base64?
    I'm trying to extend rest proxy to use Basic Authorization and Your code is just great!

    Is there maybe better way of doing that? I didn't saw any comments after last answer so I guess this is correct.

    Thanks again for sharing this code

  10. #9
    Sencha User
    Join Date
    Feb 2012
    Posts
    16
    Answers
    1
    Vote Rating
    1
    Tequiol is on a distinguished road

      0  

    Default


    Hello,

    I'm happy to hear, that this code helped you. But sorry, I've stopped using sencha touch. I cannot find my both utils classes anywhere. But it should be easy to recreate them from code that can be found using google.

  11. #10
    Sencha User Misiu's Avatar
    Join Date
    Jun 2012
    Location
    Poland
    Posts
    206
    Answers
    6
    Vote Rating
    33
    Misiu has a spectacular aura about Misiu has a spectacular aura about

      1  

    Default


    You were right, 5 minutes searching Google and some tweaks and I got this Base64 encode/decode class:

    Code:
    (function () {
        // private property
        var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
        // private method for UTF-8 encoding
        function utf8Encode(string) {
            string = string.replace(/\r\n/g, "\n");
            var utftext = "";
            for (var n = 0; n < string.length; n++) {
                var c = string.charCodeAt(n);
                if (c < 128) {
                    utftext += String.fromCharCode(c);
                } else if ((c > 127) && (c < 2048)) {
                    utftext += String.fromCharCode((c >> 6) | 192);
                    utftext += String.fromCharCode((c & 63) | 128);
                } else {
                    utftext += String.fromCharCode((c >> 12) | 224);
                    utftext += String.fromCharCode(((c >> 6) & 63) | 128);
                    utftext += String.fromCharCode((c & 63) | 128);
                }
            }
            return utftext;
        }
        // private method for UTF-8 decoding
        function utf8Decode(utftext) {
            var string = "";
            var i = 0;
            var c = c1 = c2 = 0;
            while (i < utftext.length) {
                c = utftext.charCodeAt(i);
                if (c < 128) {
                    string += String.fromCharCode(c);
                    i++;
                } else if ((c > 191) && (c < 224)) {
                    c2 = utftext.charCodeAt(i + 1);
                    string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
                    i += 2;
                } else {
                    c2 = utftext.charCodeAt(i + 1);
                    c3 = utftext.charCodeAt(i + 2);
                    string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
                    i += 3;
                }
            }
            return string;
        }
    
        Ext.define("Ext.util.Base64", {
            statics: {
                encode: (typeof btoa == 'function') ? function (input) {
                    return btoa(input);
                } : function (input) {
                    var output = "";
                    var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
                    var i = 0;
                    input = utf8Encode(input);
                    while (i < input.length) {
                        chr1 = input.charCodeAt(i++);
                        chr2 = input.charCodeAt(i++);
                        chr3 = input.charCodeAt(i++);
                        enc1 = chr1 >> 2;
                        enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
                        enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
                        enc4 = chr3 & 63;
                        if (isNaN(chr2)) {
                            enc3 = enc4 = 64;
                        } else if (isNaN(chr3)) {
                            enc4 = 64;
                        }
                        output = output +
                            keyStr.charAt(enc1) + keyStr.charAt(enc2) +
                            keyStr.charAt(enc3) + keyStr.charAt(enc4);
                    }
                    return output;
                },
                decode: (typeof atob == 'function') ? function (input) {
                    return atob(input);
                } : function (input) {
                    var output = "";
                    var chr1, chr2, chr3;
                    var enc1, enc2, enc3, enc4;
                    var i = 0;
                    input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
                    while (i < input.length) {
                        enc1 = this._keyStr.indexOf(input.charAt(i++));
                        enc2 = this._keyStr.indexOf(input.charAt(i++));
                        enc3 = this._keyStr.indexOf(input.charAt(i++));
                        enc4 = this._keyStr.indexOf(input.charAt(i++));
                        chr1 = (enc1 << 2) | (enc2 >> 4);
                        chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
                        chr3 = ((enc3 & 3) << 6) | enc4;
                        output = output + String.fromCharCode(chr1);
                        if (enc3 != 64) {
                            output = output + String.fromCharCode(chr2);
                        }
                        if (enc4 != 64) {
                            output = output + String.fromCharCode(chr3);
                        }
                    }
                    output = utf8Decode(output);
                    return output;
                }
            }
        });
    })();
    I've tested results with some online converters and they looks the same.
    Any tweaks and hints are welcome

    Here is nice MD5 implementation: http://www.sencha.com/forum/showthre...arvus.util.MD5

    BTW.
    This could be added to ExtJS code in my opinion. Any other opinions?
    Last edited by Misiu; 23 Sep 2013 at 5:59 AM. Reason: added MD5 link

Thread Participants: 2