Y a comme un écho…

Le logger ruby avec son bloc

Alors que je m'amusais à étendre le Logger de base de Ruby, j'ai découvert que l'on pouvait utiliser le Logger comme ci-dessous :

Logger.debug { "My object is #{self.map(&:id)}" }

Par défaut, on l'utilise en fournissant une string en paramètre comme ceci : Logger.debug("My object is #{self.map(&:id)}").

La différence entre ces deux écritures ?

La première permet d'éviter d'évaluer la chaine qui sera loggé si elle n'en a pas besoin. Contrairement à la deuxième qui sera toujours évaluée même si vous ne la loggez pas. Ainsi le temps de traitement pourrais s'en faire ressentir.

Grâce à ça, on peux facilement éviter les fameux :

Logger.debug("My object is #{self.map(&:id)}") if Logger.level == Logger::DEBUG

En effet, ce genre de code est très fréquent en Java, pour gagner un petit peu en performance sans perdre ses logs.

Intégration bidirectionnelle de lastfm dans spotify

Petite pensée furtive: avec spotify on peut scrobbler les titres qu'on écoute vers lastfm mais ce qui serait sympa, c'est en retour de récupérer les recommandations de lastfm (ce qui est possible avec l'API) pour proposer une radio sur mesure, ou au moins qui se rapprocherait de nos goûts.

Parce qu'aujourd'hui je ne sais pas comment est fait l'algo permettant de choisir les musiques quand on utilise la radio, mais je pense qu'il a été codé avec les pieds qu'il ne doit pas prendre en compte ce qu'on a déjà écouté.

Tests unitaires avec Capcode

devS’il y a bien un aspect, lors de la mise en place d’une application, qui obsède les développeurs, ce sont bien les tests. Et à juste titre. Au point que nous trouvons aujourd’hui de plus en plus d’équipes qui travaillent en TDD, contre toutes les lois qui nous ont été enseignées lors de nos études. L’écriture de tests unitaires peut paraitre rébarbative, cela n’est reste pas moins une très bonne habitude à prendre pour éviter par la suite de longues heures à rechercher les bugs les plus vicieux que nous avons cachés dans notre code. Je vous propose donc de voir comment faire des tests unitaires avec Capcode.

Comme vous le savez, Capcode est basé sur Rack et il existe pour ce dernier le projet Rack::Test, idéal pour ce que nous aider dans ce travail.

partons d’un exemple minimaliste :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# sample.rb
require 'rubygems'
require 'capcode'
 
module Capcode
  class Index < Route '/'
    def get
      render "Hello World"
    end
  end
 
  class Redir < Route '/r'
    def get
      redirect( Index )
    end
  end
end

Vous remarquerez que je n’ai volontairement pas mis l’appel à Capcode.run dans ce code. Ceci pour deux raisons. J’ai pris l’habitude de déporter cet appel dans un fichier à part ce qui me permet de créer en parallèle un fichier de configuration rackup pour le déploiement avec Passenger. De plus, Rack::Test sert à tester des applications basées sur Rack et implique d’avoir accès directement à l’application, celle-là même que vous passez à votre handler.

Rack::Test fonctionne avec Test::Unit, nous allons dons créer, pour chaque ensemble de tests, une classe héritant de Test::Unit::TestCase et nous inclurons dans cette classe le module Rack::Test::Methods :

1
2
3
4
5
6
7
8
9
10
11
# test_sample.rb
require 'rubygems'
require 'test/unit'
require 'rack/test'
 
class HomepageTest < Test::Unit::TestCase
  include Rack::Test::Methods
 
  # ...
 
end

Comme je l’ai signalé, l’utilisation de Rack::Test implique que nous ayons accès à l’application. Nous allons donc la charger et faire en sorte qu’elle soit accessible dans la classe via la méthode app :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# test_sample.rb
require 'rubygems'
require 'test/unit'
require 'rack/test'
 
require 'sample.rb'
@@app = Capcode.application( )
 
class HomepageTest < Test::Unit::TestCase
  include Rack::Test::Methods
 
  def app
    @@app
  end
 
  # ...
 
end

Nous pouvons ensuite ajouter nos tests. Chaque test est décrit dans une méthode de la classe, ces méthodes pouvant contenir autant d’assertions que nécessaire :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# test_sample.rb
require 'rubygems'
require 'test/unit'
require 'rack/test'
require 'sample.rb'
 
@@app = Capcode.application( )
 
class HomepageTest < Test::Unit::TestCase
  include Rack::Test::Methods
 
  def app
    @@app
  end
 
  def test_home
    get '/'
    assert_equal "http://example.org/", last_request.url
    assert last_response.ok?
    assert_equal "Hello World", last_response.body
  end
 
  def test_redirect
    get '/r'
    assert_equal "http://example.org/r", last_request.url
    follow_redirect!
    assert_equal "http://example.org/", last_request.url
    assert_equal "Hello World", last_response.body
  end
end

Si vous exécutez test_sample.rb vous devriez obtenir la sortie suivante :

Loaded suite /Users/greg/dev/Capcode/tests/test_sample
Started
.. 
Finished in 0.005839 seconds.
 
2 tests, 6 assertions, 0 failures, 0 errors

Pour les assertions, vous pouvez utiliser tout ce que propose Test::Unit. Pour la partie « navigation », Rack::Test vous offre les méthodes suivantes :

  • get, post, put, delete et head permettent respectivement de faire des requêtes de type GET, POST, PUT, DELETE et HEAD. Chacune de ces méthodes prend en paramètre l’URI à tester, mais également les paramètres, sous forme de hachage et éventuellement des données d’environnement (également sous forme de hachage).
    post '/login', {"user" => "greg", "password" => "s3cr3t"}
  • header permet de positionner la valeur d’une entête
    header "User-Agent", "Firefox"
  • basic_authorize et digest_authorize permettent de donner le login et le mot de passe pour, respectivement, une authentification basic ou digest.
    digest_authorize( "greg", "p4ssw0rd" )
  • follow_redirect! permet de suivre la dernière redirection renvoyée.
     
  • set_cookie et clear_cookie permettent de gérer les cookies.
     
  • last_request correspond à la dernière requête. C’est une instance de Rack::Request.
     
  • last_response correspond à la dernière réponse, c’est une instance de Rack::MockResponse.

Internet Explorer accepte n’importe quoi

Raison de plus pour ne plus l'accepter !

Tout à commencer par le classique:

Client: Ça marche pas quand je rentre dans ma fiche produit.
Développeur: Ça marche chez moi ©.

Voilà le contexte: L'application, en ruby on rails, permet de faire une recherche de produit. Parmi la liste des produits trouvés on peut entrer dans sa fiche et depuis celle-ci faire un export au format excel.

Là en rails on se dit que c'est assez cool à faire. On commence par enregistrer le type Mime des fichers excel dans l'initializer qui va bien (mime_types.rb):

Mime::Type.register "application/vnd.ms-excel", :xls

Ensuite dans l'action il suffit de distinguer les différents formats de rendu:

respond_to do |format|
  format.html
  format.xls {
    # some stuff
  }
end

et dans la vue utiliser la route formatée en passant l'extension enregistrée précédemment (xls).

Sur les navigateurs modernes ça fonctionne bien, sauf que pour mon client sous IE, quand il cliquait sur le lien permettant d'accéder à la fiche produit il avait directement le fichier excel.

Là je commence à me dire qu'il y a un souci dans les headers accept d'IE. Sauf que quand j'essaie de reproduire le le bug sur mon windows virtuel pas moyen. Que ce soit IE7 ou 8 ça fonctionne normalement.

Voilà à quoi ressemble les en-têtes http accept d'un IE standard (je les ai récupérés sur le net d'une personne sous windows 7):
image/jpeg, application/x-ms-application, image/gif, application/xaml+xml, image/pjpeg, application/x-ms-xbap, application/x-shockwave-flash, application/msword, */*

Là ça fonctionne, mon lien « classique » ne correspond à aucune des type mime précédent, du coup je passe dans le * et pour IE comme il ne sait pas trop quoi en faire il le traite comme du HTML. Que je sache pour les autres navigateurs c'est l'inverse. On regarde d'abord si ce n'est pas du html, du xhtml+xml ou du xml et après on avise.

Mais pour Internet Explorer le web c'est d'abord des documents binaires, pas du texte.

Pour en revenir au problème là ou ça devient vicieux c'est que les en-têtes http accept sont différents si Office est installé sur la machine (ce qui est le cas pour client et pas moi). Là ça devient:
image/gif, image/jpeg, image/pjpeg, application/x-ms-application, application/vnd.ms-xpsdocument, application/xaml+xml, application/x-ms-xbap, application/x-shockwave-flash, application/x-silverlight-2-b2, application/x-silverlight, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*

C'est la que le format excel prend le pas. Donc dans la négociation avec le serveur, IE demande en priorité du format excel plutôt qu'html.

Ca a été remonté sur le tracker de ruby on rails, même si ça n'a rien à voir avec le framework. Toutefois une solution est proposé pour contourner ce comportement d'IE, c'est de forcer le passage du paramètre format à HTML s'il n'est pas défini. Il faut savoir que ce n'est pas fiable à 100% puisque ça se base sur le user agent qui peut être modifié. Mais forcer le format à html pour les navigateurs qui fonctionne déjà correctement ne devrait pas avoir de conséquences fâcheuses.

Avant de tomber sur cette solution j'ai essayé d'utiliser une route formatée dans laquelle je spécifié html comme format mais ça n'a pas fonctionné. Je me retrouvais bien avec un lien se terminant par .html mais le format n'était toujours pas récupéré dans les paramètres de l'action, je ne sais pas vraiment pourquoi.

Bref, encore un post sur le net pour dire une chose: « Meurs IE, meurs ! »

Tests unitaires avec Capcode

Quatorzième Apéro Ruby de Paris.rb / Ruby France - spécial Sinatra

Paris.rb, groupe local de l’association Ruby France, organise son quatorzième Apéro Ruby, ouvert à tous, rubyistes débutants ou confirmés. Cette rencontre se déroulera le jeudi 15 octobre 2009, à partir de 20h, à Dune , 18, avenue Claude Vellefaux 75010 Paris, Métro Colonel Fabien ou Métro Goncourt (voir plan ou voir sur Google Maps Street View ).

Nous profitons de la présence ce mois-ci de Simon Rozet , committer Sinatra pour faire une soirée dédiée à ce framework web léger. Nous ferons un dojo , c’est-à-dire une séance pratique basée sur le Test-Driven Development, Sinatra et Rack::Test (éventuellement cucumber aussi). Vous pouvez venir avec votre ordinateurs portable.

Simon fera un lightning talk sur Rip , Bundler et les bonnes pratiques liées à RubyGems. Peut-être aurons-nous le temps de parler d’Integrity , de Mustache , ou d’Unicorn .

Il n’est pas obligatoire de m’indiquer votre venue (il y a suffisamment de place) mais si vous le souhaitez par politesse, vous pouvez le faire : mon adresse jftran (at) rubyfrance (point) org ou twitter @underflow_.

À bientôt !

PS. Il y aura peut-être le même soir un Apéro Ruby à Lyon, stay tuned.

Quatorzième Apéro Ruby de Paris.rb / Ruby France - spécial Sinatra

Paris.rb, groupe local de l’association Ruby France, organise son quatorzième Apéro Ruby, ouvert à tous, rubyistes débutants ou confirmés. Cette rencontre se déroulera le jeudi 15 octobre 2009, à partir de 20h, à Dune , 18, avenue Claude Vellefaux 75010 Paris, Métro Colonel Fabien ou Métro Goncourt (voir plan ou voir sur Google Maps Street View ).

Nous profitons de la présence ce mois-ci de Simon Rozet , committer Sinatra pour faire une soirée dédiée à ce framework web léger. Nous ferons un dojo , c’est-à-dire une séance pratique basée sur le Test-Driven Development, Sinatra et Rack::Test (éventuellement cucumber aussi). Vous pouvez venir avec votre ordinateurs portable.

Simon fera un lightning talk sur Rip , Bundler et les bonnes pratiques liées à RubyGems. Peut-être aurons-nous le temps de parler d’Integrity , de Mustache , ou d’Unicorn .

Il n’est pas obligatoire de m’indiquer votre venue (il y a suffisamment de place) mais si vous le souhaitez par politesse, vous pouvez le faire : mon adresse jftran (at) rubyfrance (point) org ou twitter @underflow_.

À bientôt !

PS. Il y aura peut-être le même soir un Apéro Ruby à Lyon, stay tuned.

Authentification HTTP avec Capcode

projetsQuand j’ai mis en place le moteur de rendu WebDAV dans Capcode, la première remarque qui m’est revenue fut « Oui, mais comment faire pour sécuriser un peu tout ça ?« . Bonne question dont malheureusement la réponse fut beaucoup plus facile à mettre en place que ce que j’avais imaginé1.

La sécurisation d’un serveur WebDAV passe par de l’authentification HTTP. Il existe une solution dans rack, via Rack::Auth. Malheureusement, non seulement la documentation est un élément qui fait cruellement défaut dans rack, mais en plus s’il est facile de mettre en place une authentification globale pour toute une application, c’est une autre paire de manches quand on souhaite faire cela pour une route précise. En effet, il n’est pas rare d’avoir, dans un site, une partie publique et une autre privée. Et c’est bien ce que je souhaitais permettre.

En cherchant sur Google, je suis tombé sur cet excellent post expliquant comment mettre en place une authentification Basic dans Sinatra. J’ai donc adapté la solution pour Capcode. Le problème de l’authentification Basic c’est qu’elle est plutôt réservée aux sites accessibles en HTTPS puisque les mots de passent sont en clair dans l’entête. J’ai donc fait un petit passage par la RFC26172 et en lisant attentivement le code de Rack::Auth::Digest::Request et Rack::Auth::Digest::MD5, j’ai rapidement ajouté l’authentification Digest. Ceci a donné naissance à un helper pour les controôleurs : http_authentication.

La mise en place est donc très simple. Voici un petit exemple :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
require 'capcode'
require 'rack'
 
module Capcode
  class Private < Route '/private'    
    def get
      # Basic HTTP Authentication
      http_authentication( :type => :basic, :realm => "My Capcode test!!!" ) { 
        {
          "greg" => "toto",
          "mu" => "maia"
        }
      }
      render "Success !"
    end
  end 
 
  class Public < Route '/public'
    def get
      render "You don't need any special authorization here !"
    end
  end
end
 
Capcode.run( )

La partie importante se trouve de la ligne 8 à la ligne 13.

http_authentication permet de spécifier que vous voulez mettre en place une authentification HTTP pour le contrôleur Private. Ce helper supporte les options suivantes :

  • :type qui permet de préciser le type d’authentification. Les valeurs possibles sont :basic (valeur par défaut) et :digest.
  • :realm qui donne l’information sur le nom et mot de passe que peut utiliser l’utilisateur. C’est une chaine de caractère pouvant prendre n’importe quelle valeur ( »Capcode.app » par défaut).
  • :opaque qui est une chaîne générée par le serveur devant être retournée telle quelle par le client (defaut : « opaque »). Cette option n’est utile que dans le cas d’une authentification Digest.

Enfin, http_authentication prend en paramètre un bloc qui doit lui retourner un hachage contenant les couples login/mot de passe pouvant s’authentifier, sous la forme :

{
  "user1" => "pass1",
  "user2" => "pass2",
  # ...
}

Dans l’exemple ci-dessus, nous avons mis en place une authentification Basic. Aux vues de ce qui vient d’être dit, vous voyez aisément comment passer à une authentification de type Digest :

7
8
9
10
11
12
13
# Digest HTTP Authentication
http_authentication( :type => :digest, :opaque => "Ma phrase secrète", :realm => "My Capcode test!!!" ) { 
  {
    "greg" => "toto",
    "mu" => "maia"
  }
}

Tout cela c’est très bien me direz vous, mais comment je fais si je veux protéger plusieurs routes avec une même authentification. Prenons l’exemple suivant :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
require 'capcode'
 
module Capcode
  class Public < Route '/public'
    def get
      render "You don't need any special authorization here !"
    end
  end
 
  class Private < Route '/private'
    def get
      render "Welcome to the private part of this site !"
    end
  end
 
  class PrivateAgain < Route '/private/again'
    def get
      render "Welcome to the private/again part of this site !"
    end
  end
 
end
 
Capcode.run( )

Avec ce qui a été dit ci-dessus, si nous voulons protéger les parties Private et PrivateAgain, nous devons utiliser deux fois http_authentication. Essayez de le mettre seulement dans Private et vous verrez que vous pouvez accéder à PrivateAgain sans aucun problème. Ce qui est un problème !!!

Heureusement, il y a une solution. En effet, vous pouvez faire une déclaration de demande d’authentification de manière globale. Pour cela j’ai mis en place une méthode Capcode::http_authentication qui ressemble en tout point au helper utilisable dans les contrôleurs, mais prenant un paramètre de plus : :routes. Ainsi, vous pouvez mettre en place la protection souhaitée de la façon suivante :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
require 'capcode'
require 'digest/md5'
 
module Capcode
 
  OPAQUE = Digest::MD5.hexdigest( Time.now.to_s )
  http_authentication( :type => :digest, :opaque => OPAQUE, :realm => "Private parts", :routes => "/private" ) { 
    {
      "greg" => "toto",
      "mu" => "maia"
    }
  }
 
  class Public < Route '/public'
    def get
      render "You don't need any special authorization here !"
    end
  end
 
  class Private < Route '/private'
    def get
      render "Welcome to the private part of this site !"
    end
  end
 
  class PrivateAgain < Route '/private/again'
    def get
      render "Welcome to the private/again part of this site !"
    end
  end
 
end
 
Capcode.run( )

A la ligne 7, nous utilisons la méthode http_authentication pour dire que nous voulons une protection pour toutes les routes ayant pour racine /private. En terme d’expression régulière cela veut dire que si la route match avec /^\/private([\/]{1}.*)?$ alors il faudra s’authentifier pour y accéder.

Notez également que le paramètre :routes peut prendre comme valeur un tableau, ce qui peut grandement nous faciliter la vie si nous voulons utiliser une protection commune pour des routes n’ayant pas la même racine.

Et notre serveur WebDAV alors ? Et bien voici sa nouvelle version :

# file: sample.rb
require 'rubygems'
require 'capcode'
require 'capcode/render/webdav'
 
module Capcode
  # !!! Render file from /Users/greg/Documents/etudes !!!
  class WebDav < Route '/etudes'
    def get
      http_authentication( :type => :digest, :realm => "Greg' WebDAV Server" ) { 
        {
          "greg" => "my super secret password",
          "commercial" => "semaine 23"
        }
      }
      render :webdav => "/Users/greg/Documents"
    end
    def method_missing(id, *a, &b); get; end
  end  
end

Dernière petite chose avant de terminer, sachez que vous pouvez récupérer le login utilisé via request.env['REMOTE_USER'].

1 bon, j’ai un jour de retard, mais quand même !!!
2 Lire également l’article HTTP Authentification de Wikipedia.

Authentification HTTP avec Capcode