3.1 - Service REST


Échange de données

Service REST

Plan

  • Ressources
  • Status
  • Header

REST

REST propose une architecture standard pour l'élaboration d'application serveur

  • Interface uniforme pour l'accès aux ressources
  • Séparation client-serveur

Le protocole HTTP offre plusieurs mécanismes permettant de respecter les principes REST en développant un service web respecter les principes REST en développant un service web

  • URL
  • Verbe
  • Status
  • Headers
  • Body

HTTP, récapitulatif

Requête
Réponse

URL

La structure des URLs permet d'identifier clairement et de manière uniforme les ressources accessible via le service web.

/products
/products?brand=keychron&sort=price-asc
/products/B09NLTWKGP


/semesters
/semesters/A2020
/semesters/A2020/courses
/semesters/A2020/courses/420-0Q7-SW
/semesters/A2020/courses/420-0Q7-SW/groups
/semesters/A2020/courses/420-0Q7-SW/groups/2


/orders/202201012314
/orders/202201012314/invoice

Verbe

Les verbes HTTP permettent de rendre explicite l'action à appliquer sur une ressource

  • GET Récupérer
  • POST Créer, soumettre
  • PUT Remplacer sinon créer
  • PATCH Modifier partiellement
  • DELETE Supprimer
GET /conversations

GET /conversations/9812
GET /conversations/9812/messages

POST /conversations/9812/messages
{ "text": ... }

PATCH /messages/987123654
{ "text" : ... }

PATCH /messages/987123654
{ "reaction" : ... }

DELETE /messages/456192837

Verbes

Status

  • 1XX Information
  • 2XX Succès 200, 201, 202, 204
  • 3XX Redirection
  • 4XX Erreur du client 400, 401, 403, 404, 405, 429
  • 5XX Erreur du serveur

Status Codes

ATTENTION

Il est aussi possible de TOUJOURS retourner un code 200 et d'indiquer l'erreur dans le body de la réponse.

Headers

Les headers permettent d'ajouter des informations complémentaires à la communication HTTP, requête ou réponse, identifiés par la structure Clé: Valeur

  • Contenu
  • Authentification
  • Contexte requête/réponse
  • Cookies
  • Cache
Content-Length: 742

Content-Type: application/json
Content-Type: text/html

Content-Disposition: attachment; filename="cat.jpg"

Accept: */*
Accept: image/*
Accept: text/html

Headers

Body

Le mécanisme principal de transfert de données en HTTP est le body des requêtes et réponses. Outre les fichiers bruts (images, zip, etc.), il est judicieux d'uniformiser le format de donnée utilisé.

  • JSON
  • XML
  • Protobuf
  • ...

Exemple RPC -> REST

👉

Exemple RPC -> REST

# BUCKET LISTS
GET /lists

POST /list-add
voyages

POST /list-edit
voyages
destinations

POST /list-delete
nourriture

# ITEMS D'UNE LISTE
GET /lists-items?list=...
GET /lists-items
destinations

POST /lists-items?list=...
nouvel item
POST /lists-items
destinations=allemagne

POST /list-items-delete
_ITEM ID_
# BUCKET LISTS
GET /lists

POST /lists
{ name: _NAME_ }

PATCH /lists/_NAME_
{ name: _NEW NAME_ }

DELETE /lists/_NAME_

# ITEMS D'UNE LISTE
GET /lists/_LIST NAME_/items
? GET /lists/items?list=_LIST NAME_

POST /lists/_NAME_/items
{ item: ... }
? POST /lists/items?list=_LIST NAME_
{ item: ... }

DELETE /lists/_NAME_/items/_ID_
DELETE /items/_ID_

Et les réponses?

Démo

Serveur

require "bundler/inline"

gemfile do
    source "http://rubygems.org"
    gem "sinatra-contrib"
    gem "webrick"
end

require 'sinatra'
require 'sinatra/reloader' if development?

#
# VERBE
#

get "/" do
  return "Bonjour à tous!"
end 

# Une route est identifiee par le VERBE + CHEMIN
# GET / est different de DELETE /
delete "/" do
  return "Ceci est une action DELETE"
end

#
# Parametre de chemin
#
get "/greetings/admin" do
  return "Bonjour cher admin!"
end

# ATTENTION, l'ordre de declaration des routes est important
# /greetings/admin
# /greetings/james
get "/greetings/:name" do
  # On peut preciser un parametre dans le chemin

  return "Bonjour à #{params["name"]}!"
end 

#
# Réponse: Status et Headers
#

# An Array with three elements: [status (Integer), headers (Hash), response body (responds to #each)]
# An Array with two elements: [status (Integer), response body (responds to #each)]
# An object that responds to #each and passes nothing but strings to the given block
# A Integer representing the status code

get "/status" do
  return 204
end

get "/status/body" do
  return [200, "Code 200, Tout est OK!"]
end

get "/status/body/headers" do
  data = { name: "James Hoffman", title: "Enseignant", age: 42 }
  headers = {
    "Content-Type"=> "application/json"
  } 

  return [
    200,
    headers,
    data.to_json
  ]
end

#
# Requête: Headers
#

post "/headers" do
  # Les headers sont formatés selon les règles suivantes
  # - Nom en MAJUSCULE
  # - Les tirets - deviennent des barres de soulignement _
  # - Préfix HTTP_ 
  # Exemple: 
  #    Accept devient HTTP_ACCEPT
  #    User-Agent devient HTTP_USER_AGENT
  #
  # EXCEPTIONS
  #    CONTENT_LENGTH, CONTENT_TYPE

  # Les données complémentaires de la requête
  # sont disponible dans le Hash request.env
  # Plusieurs helpers simplifient l'accès
  # https://www.rubydoc.info/github/rack/rack/Rack/Request/Helpers
  return [
    request.content_type,

    request.user_agent,

    request.accept?("application/json").to_s,

    request.has_header?("CONTENT_TYPE").to_s,
    request.get_header("CONTENT_TYPE"),

    request.get_header("HTTP_MY_HEADER"),

    request.env.to_s,
    request.env["HTTP_MY_HEADER"]
  ].join("\n\n\n\n")
end

Client

require "bundler/inline"

gemfile do
    source "http://rubygems.org"
    gem "faraday"
end

require "faraday"


server = Faraday.new(url: "http://localhost:4567")

def show(response)
  puts(response.status)
  puts("---")
  puts(response.headers)
  puts("---")
  puts(response.body)
  puts("\n\n")
end

show(server.get("/"))

show(server.delete("/"))

show(server.get("/greetings/admin"))

show(server.get("/greetings/james"))

show(server.get("/status"))

show(server.get("/status/body"))

show(server.get("/status/body/headers"))

headers = {
  "Content-Type" => "custom/james",
  "Accept" => "text/html",
  "My-Header" => "ahoy!"
}
show(server.post("/headers", nil, headers))

💻 Recipeasy

Authentification

Il est fréquent de vouloir restreindre l'accès à certaines ressources d'un service web, donc il faut être en mesure d'authentifier qui effectue la requête. On peut mettre en place une technique personnalisée pour la gestion des identifiants, mais le protocole HTTP offre un mécanisme standard qui facilite l'implémentation via les headers.

Réponse initiale

WWW-Authenticate: Basic realm="Zone restreinte!!!"

Requête subséquente

Authorization: Basic ZXRkOnNoYXdp

Les identifiants sont joints par : et encodé en Base64, ex: etd:shawi devient ZXRkOnNoYXdp

Démo Auth

Serveur Auth

require "bundler/inline"

gemfile do
    source "http://rubygems.org"
    gem "sinatra-contrib"
    gem "webrick"
end

require 'sinatra'
require 'sinatra/reloader' if development?

#
# Authentification
#
# https://sinatrarb.com/faq.html#auth

# Pour toutes les routes
# ATTENTION, il faut redémarrer le serveur après avoir
# activé l'authentification user Rack::Auth::Basic
# use Rack::Auth::Basic do |username, password|
#   # Le mecanisme de validation
#   # peut etre personnalisé, ex:
#   # fichier, BD, etc.
#   username == "admin" && password == "pwda"
# end
# 
# get "/secret" do
#  "Hi!"
# end
# 
# post "/also-secret" do
#  "Hi again!"
# end


# Authentification personnalisée, configurée par route
require "openssl"
SHA_KEY = "th3Superkey!" # Salt de hachage

# On ne veut pas enregistrer de mot de passe en clair
# le hachage sha256 permet d'empecher une fuite
# des mots de passe pouvant amener a une attaque subsequente si réutilisé
# https://dzone.com/articles/why-we-hash-passwords
def sha256(value)
  nil if value.nil? || value.empty?

  OpenSSL::HMAC.hexdigest("sha256", SHA_KEY, value)
end

def guard!
  auth_required = [
    401,
    {
      "WWW-Authenticate" => "Basic"
    },
    "Provide a username and password throught Basic HTTP authentication"
  ]

  # HALT interrompt IMMEDIATEMENT la requête et retourne le resultat
  halt auth_required unless authorized?
end

def authorized?
  auth ||=  Rack::Auth::Basic::Request.new(request.env) 
  # request.env est disponible car on est dans le block HELPERS

  return unless auth.provided? && auth.basic? && auth.credentials

  username, password = auth.credentials # Structure est ["admin", "pwda"]

  if username == "admin" && sha256(password) == "71b0e3e2c9d25193a93bf31beaed7c3cf08d2d39a37e52f79366af1b7f6bf9d6" # sha256("pwda")
    @user = username
  end
end

get "/public" do
  "Hey folks!"
end

get "/private" do
  guard! # Applique l'authentification pour cette route

  "Ahoy #{@user}"
end

Client Auth

require "bundler/inline"

gemfile do
    source "http://rubygems.org"
    gem "faraday"
end

require "faraday"

server = Faraday.new(url: "http://localhost:4567")

def show(response)
  puts(response.status)
  puts("---")
  puts(response.headers)
  puts("---")
  puts(response.body)
  puts("\n\n")
end

show(server.get("/public"))

show(server.get("/private"))

# Auth manuelle
require "base64"
show(server.get("/private", nil, {"Authorization" => "Basic #{Base64.encode64("admin:pwda")}"}))

# Helper sur la connexion
# ATTENTION, tant que la connexion existe,
# les infos sont utilisées

server.set_basic_auth("admin", "pwda")
show( server.get("/private"))

# On peut egalement configurer l'authentification
# a la creation de la connexion https://lostisland.github.io/faraday/#/middleware/included/authentication

💻 Recipeasy, suite