json

danigm's picture

Últimamente he estado jugando con JQuery y llamadas ajax a servcios json y me he encontrado con un pequeño problema. Cuando intentas acceder a un servicio que te ofrece json plano jquery peta y te devuelve un error como este en la consola de javascript del navegador:


Error: uncaught exception: [Exception... "Access to restricted URI denied" code: "1012" nsresult: "0x805303f4 (NS_ERROR_DOM_BAD_URI)" location: "js/jquery-1.3.2.min.js Line: 19"]

Esto ocurre porque el navegador implementa una política de seguridad mediante la cual no permite que se acceda a urls diferentes a la que hace la petición. Osea que no puedes hacer peticiones a un servidor externo de json pelado.

Para realizar este tipo de peticiones existen callbacks y jsonp. La idea es que en la url de la petición jquery debes añadir "http://.../?callback=?" y jquery rellenará el resto. Para cada petición jquery pasará como callback un nombre de función "jsonp1234" donde los números son diferentes por petición y el servidor ha de responder con el código json envuelto en esa función "jsonp1234({...});".

¿Qué haces cuando el servidor no ofrece el servicio jsonp?

Aquí es donde viene el proxy guarrón que me he implementado. La idea es pillar todas las peticiones al servidor, pillar el json plano y modificarlo para que sea jsonp.

Para ello lo primero que desarrollé fué un proxy http plano y feo:

  1. # python proxy.py twitter.com 80
  2. import sys
  3. import socket
  4.  
  5. s = socket.socket()
  6. mserver = 'localhost'
  7. mport = 8080
  8. s.bind((mserver, mport))
  9. s.listen(10)
  10.  
  11. server = sys.argv[1]
  12. port = int(sys.argv[2])
  13.  
  14. while True:
  15. try:
  16. print "waiting for requests at %s:%s" % (mserver, mport)
  17. s1 = s.accept()
  18. except KeyboardInterrupt:
  19. s.close()
  20. print "bye"
  21. break
  22. s2 = socket.socket()
  23. s2.connect((server, port))
  24.  
  25. s1 = s1[0]
  26. v = ''
  27. r = 0
  28. while not r or len(r) == 1024:
  29. r = s1.recv(1024)
  30. v += r
  31.  
  32. v = v.replace('Host: %s:%s' %(mserver, mport), 'Host: %s:%s' % (server, port))
  33. print v
  34. s2.send(v)
  35.  
  36. v2 = ''
  37. r = 0
  38. while True:
  39. s2.settimeout(0.3)
  40. try:
  41. r = s2.recv(1024)
  42. except:
  43. break
  44. v2 += r
  45.  
  46. s1.send(v2)
  47. s2.close()
  48. s1.close()

¿Qué hace? pues monta un servidor en el puerto 8080 y cada petición a este servidor la hace directamente al servidor remoto y la respuesta de este es lo que devuelve. Simple ¿verdad?.

Pero yo quiero modificar lo que me devuelve, así que hay que tratar la respuesta del servidor remoto:

  1. #!/usr/bin/python
  2.  
  3. # proxy.py twitter.com 8080
  4.  
  5. import sys
  6. import socket
  7. import gzip
  8. import StringIO
  9. import re
  10.  
  11. pet = re.compile('(GET) (?P<url>.*)\?(?P<args>.*) (HTTP/1.1)')
  12. js = re.compile('(\{.*\})')
  13.  
  14. def html(out):
  15. ret = ''
  16. ret += "Content-type: text/html\r\n"
  17. ret += 'Content-Encoding: gzip\r\n'
  18. ret += 'Content-Length: %s\r\n' % len(out)
  19. ret += '\r\n'
  20. ret += out
  21. return ret
  22.  
  23. def compressBuf(buf):
  24. zbuf = StringIO.StringIO()
  25. zfile = gzip.GzipFile(mode = 'wb', fileobj = zbuf, compresslevel = 9)
  26. zfile.write(buf)
  27. zfile.close()
  28. return html(zbuf.getvalue())
  29.  
  30. def json(cad, callback="json"):
  31. if not callback:
  32. return cad
  33. else:
  34. return "%s(%s);" % (callback, cad)
  35.  
  36. s = socket.socket()
  37. mserver = 'localhost'
  38. mport = 8080
  39. s.bind((mserver, mport))
  40. s.listen(10)
  41.  
  42. server = sys.argv[1]
  43. port = int(sys.argv[2])
  44.  
  45. while True:
  46. try:
  47. print "waiting for requests at %s:%s" % (mserver, mport)
  48. s1 = s.accept()
  49. except KeyboardInterrupt:
  50. s.close()
  51. print "bye"
  52. break
  53. s2 = socket.socket()
  54. s2.connect((server, port))
  55.  
  56. s1 = s1[0]
  57. v = ''
  58. r = 0
  59. while not r or len(r) == 1024:
  60. r = s1.recv(1024)
  61. v += r
  62.  
  63. v = v.replace('Host: %s:%s' %(mserver, mport), 'Host: %s:%s' % (server, port))
  64. callback=''
  65. args = pet.search(v)
  66. if args:
  67. args = args.group('args').split('&')
  68. for a in args:
  69. c,val = a.split('=')
  70. if c == 'callback':
  71. callback = val
  72. break
  73. v = pet.sub(r'\1 \2 \4', v)
  74. print v
  75. s2.send(v)
  76.  
  77. v2 = ''
  78. r = True
  79. while r:
  80. s2.settimeout(1)
  81. try:
  82. r = s2.recv(1024)
  83. except:
  84. break
  85. v2 += r
  86.  
  87. v3 = v2.split('\r\n')
  88. print v3
  89. encoding = ''
  90. for i in v3:
  91. if i.startswith('Content-Encoding: '):
  92. encoding = i[len('Content-Encoding: '):]
  93. if encoding == 'gzip':
  94. st = StringIO.StringIO(v3[-1])
  95. content = gzip.GzipFile(fileobj=st).read()
  96. else:
  97. try:
  98. content = js.search(v2).groups()[0]
  99. except:
  100. content = ''
  101.  
  102. print content
  103.  
  104. s1.send(json(content, callback))
  105. s2.close()
  106. s1.close()

Puedes probar con twitter por ejemplo, lanzando el proxy "python proxy.py twitter.com 80" y haciendo la petición desde el navegador "http://localhost:8080/status/user_timeline/danigm.json?callback=jsonp123" o "http://localhost:8080/status/user_timeline/danigm.json?callback=holaaaaa".

Y jugando con esto puedes hacer un proxy que modifique toda respuesta, por lo que te puedes divertir mucho.

Por supuesto habrá muchos proxys http hechos en python, pero este lo he hecho yo, a bajo nivel y de mala manera :P