REST propose une architecture standard pour l'élaboration d'application 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
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
Les verbes HTTP permettent de rendre explicite l'action à appliquer sur une ressource
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
200, 201, 202, 204
400, 401, 403, 404, 405, 429
ATTENTION
Il est aussi possible de TOUJOURS retourner un code 200 et d'indiquer l'erreur dans le body de la réponse.
Les headers permettent d'ajouter des informations complémentaires à la communication HTTP, requête ou réponse, identifiés par la structure Clé: Valeur
Content-Length: 742
Content-Type: application/json
Content-Type: text/html
Content-Disposition: attachment; filename="cat.jpg"
Accept: */*
Accept: image/*
Accept: text/html
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é.
# 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?
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
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))
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
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
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