Part 01 · Backend Mindset
Client vs Server
কে কোন দায়িত্ব নেয়, আর browser আর backend এর মাঝের boundary আসলে কোথায়।
⏱ 1 ঘণ্টা · Station 2 / 153
দাম ছিল 500। কেউ দিলো 1।
তোমার একটা ছোট online বইয়ের দোকান। একটা বইয়ের দাম 500 টাকা। এক সকালে order গুলো দেখতে গিয়ে পেলে, একজন customer দুটো বই কিনেছে আর দিয়েছে 2 টাকা।
কেউ তোমার server hack করেনি। কেউ password আন্দাজ করেনি। App টা order এর সাথে দামটাও পাঠাতো, qty=2&price=500, আর server যা পেতো তাই গুণ করতো। এই customer শুধু price=1 পাঠিয়েছে।
তোমার app এ তো দাম লেখার কোনো ঘরই নেই, তাহলে এটা সম্ভব হলো কীভাবে? আর দোষটা কার: app এর, নাকি server এর?
এই lesson দুটো প্রশ্নেরই উত্তর দেয়, তুমি যত system বানাবে তার প্রতিটার মাঝখান দিয়ে একটা লাইন টেনে: client আর server এর মাঝের লাইন।
সমস্যা: দুটো program, দুজন মালিক
Software যখন অনেক user কে serve করে (lesson 1), তখন সেটা দুটো program এ ভাগ হয়ে যায়, যারা একদম আলাদা জায়গায় চলে।
| Client | Server | |
|---|---|---|
| কোথায় চলে | User এর phone, laptop বা browser এ | তোমার নিয়ন্ত্রণে থাকা একটা machine এ |
| কে নিয়ন্ত্রণ করে | User। সে এটা খুলে দেখতে পারে, বদলাতে পারে, পুরোটা অন্য কিছু দিয়ে বদলেও দিতে পারে | তুমি, শুধু তুমি |
| কয়টা copy | প্রতি user এ একটা, লাখ লাখ, অনেকগুলো পুরনো version | একটা, বা অল্প কয়েকটা একই রকম |
| কীসে ভালো | জিনিস দেখানো, tap আর key এ সাথে সাথে সাড়া দেওয়া | Shared data রাখা, rule check করা, secret লুকিয়ে রাখা |
| বিশ্বাস করা যায়? | না | হ্যাঁ, যদি যত্ন করে বানাও |
যে লাইনটা মানুষকে অবাক করে সেটা হলো "কে নিয়ন্ত্রণ করে"। তোমার app একজন অচেনা মানুষের device এ install করা। ওখানে যাওয়ার পর ওটা তার। সে app এর code পড়তে পারে, app যত request পাঠায় সব দেখতে পারে, আর curl এর মতো tool দিয়ে নিজেই শুরু থেকে request লিখতে পারে, তোমার app কে পুরো বাদ দিয়ে।
তাই server আসলে কখনো তোমার app এর সাথে কথা বলে না। সে কথা বলে যে program ই তাকে bytes পাঠায় তার সাথে।
একটা restaurant ভাবো
Restaurant এ তুমি menu হাতে টেবিলে বসো। পছন্দ করো, আঙুল দিয়ে দেখাও, প্রশ্ন করো। এটাই client: যেখানে তুমি interact করো, আর যেভাবে খুশি ব্যবহার করতে পারো।
রান্নাঘরটা হলো server। ওখানে থাকে বাজার-সদাই (data), রেসিপি (rule) আর ক্যাশ (টাকা)। তুমি রান্নাঘরে ঢোকো না। তুমি একটা order পাঠাও, আর আসলে কী হবে সেটা রান্নাঘর ঠিক করে।
এবার ভাবো order slip এ একটা লাইন আছে যেখানে তুমি দাম লেখো। বেশিরভাগ customer সৎভাবে menu থেকে দাম তুলবে। একজন লিখবে "1 টাকা"। রান্নাঘর যদি slip এ যা লেখা তাই নেয়, সেটা customer এর চালাকি না, restaurant এর design এর ভুল।
Analogy টা কোথায় খাটে না। একজন waiter customer কে দেখে কিছু অদ্ভুত লাগলে টের পায়। Server কাউকে দেখতে পায় না: প্রতিটা request শুধু text, আর attacker এর লেখা request দেখতে হুবহু তোমার app এর পাঠানো request এর মতো। দেখার মতো কোনো "চেহারা" নেই। তাই rule টা server এর ভেতরেই বানিয়ে রাখতে হয়।
মূল জিনিসগুলো
যেকোনো app কে খুলে ফেললে একই কয়েকটা টুকরো পাবে।
- Request। Client একটা message পাঠায়: "user এটা বেছেছে।" বাস্তবে সেটা একটা HTTP request: একটা method, একটা path, কিছু header আর হয়তো একটা body। (Module 3।)
- Response। Server উত্তর দেয়: "এটা হলো।" একটা status code, header আর body।
- Boundary। Client এর দিকের সবকিছু input: কাজের, কিন্তু check করা না। Server এর দিকের সবকিছু সিদ্ধান্ত: দাম কত, তুমি এটা করতে পারবে কিনা, কী save হবে।
- দায়িত্ব। প্রতিটা দিকের একটা কাজ আছে যেটায় সে সবচেয়ে ভালো:
| Client এর কাজ | Server এর কাজ |
|---|---|
| Screen আঁকা, layout, animation | দাম, মোট টাকা, discount, stock |
| সাথে সাথে feedback ("email টা ভুল মনে হচ্ছে") | আসল check, প্রতিবার |
| খোলা tab বা draft মনে রাখা | User দের মধ্যে shared data |
| যে button user ব্যবহার করতে পারবে না সেটা লুকানো | তবুও চেষ্টা করলে কাজটা ফিরিয়ে দেওয়া |
| গোপন কিছুই না | API key, password, অন্যদের data |
- মূল নিয়ম। Client এ check করো user এর সুবিধার জন্য; server এ check করো যাতে জিনিসটা আসলেই ঠিক থাকে। Form বলতে পারে "quantity 1 থেকে 10 হতে হবে", যাতে সৎ user তাড়াতাড়ি feedback পায়। Server কে একই জিনিস আবার check করতে হবে, কারণ form টা এড়িয়ে যাওয়া যায়।
তোমার checkout form JavaScript দিয়ে 10 এর বেশি quantity আটকায়। একজন user browser এর developer tools খুলে সরাসরি qty=500 পাঠালো। Order টা যাবে কিনা কে ঠিক করবে?
ভেতরে কী হয়: একটা order, দুটো পথ
একই order দুবার আসছে: একবার তোমার app থেকে, একবার হাতে লেখা।
একই order, দুইভাবে পাঠানো
ধাপ 1/6 User "২টা কিনবো" তে tap করলো
User যা বেছেছে তা দিয়ে app একটা request বানায়। যত্নশীল app শুধু পছন্দটা পাঠায়: কোন বই আর কয়টা। দাম না।
- request
- GET /checkout?item=book&qty=2
মাথায় রাখার ছবি
তোমার user আর তোমার server এর মাঝে একটা খাড়া লাইন আঁকো। নাম দাও trust boundary।
- লাইনের বাঁ দিকে: তোমার app এর হাজারটা copy, এমন device এ যেগুলো তোমার নিয়ন্ত্রণে নেই, কিছু বদলানো, কিছু পুরনো, কিছু তো তোমার app ই না।
- লাইনের ডান দিকে: একটা program যেটা তোমার নিয়ন্ত্রণে, data আর rule সহ।
- বাঁ থেকে ডানে যা কিছু লাইন পার হয় সব একেকটা দাবি: "user এটা চায়"। Server প্রতিটা দাবি check করে, প্রতিবার।
যখনই নতুন feature যোগ করবে, নিজেকে প্রশ্ন করো: কেউ যদি আমার app বাদ দিয়ে হাতে এই request পাঠায়, সবচেয়ে খারাপ কী করতে পারবে? উত্তর যদি হয় "দাম বদলানো", "অন্যের data পড়া" বা "payment এড়িয়ে যাওয়া", তাহলে ওই logic লাইনের ভুল দিকে আছে।
চেষ্টা করো: কোন দিক সিদ্ধান্ত নেবে?
প্রতিটা card একটা real app এর এক টুকরো logic। উত্তর পড়ার আগে ঠিক করো এটা কোথায় থাকা উচিত।
Client, server, নাকি দুটোই?
client: টাইপ করার সময়ই feedback server: submit এর সময় email আবার check করে
গোড়া থেকে code
দুটো endpoint সহ একটা ছোট দোকানের server, শুধু standard library দিয়ে বানানো:
/trustclient যা quantity আর দাম পাঠায় তাই গুণ করে।/checkoutশুধু item আর quantity পড়ে, দাম নিজে খোঁজে, আর quantity check করে।
তারপর তিনটা request হাতে লেখা হয়, TCP socket এ raw text হিসেবে, ঠিক যেভাবে curl বা একজন attacker এর script লিখতো। প্রথমটা তোমার app যা পাঠায়। বাকি দুটো না।
package main
import (
"bufio"
"fmt"
"net"
"net/http"
"strconv"
"strings"
)
// The price lives on the server. The client never gets to decide it.
var prices = map[string]int{"book": 500}
// /trust believes whatever the client sent, price included.
func trust(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
qty, _ := strconv.Atoi(q.Get("qty"))
price, _ := strconv.Atoi(q.Get("price"))
fmt.Fprintf(w, "charged %d", qty*price)
}
// /checkout takes only what the user chose, and decides everything else itself.
func checkout(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
price, ok := prices[q.Get("item")]
if !ok {
http.Error(w, "no such item", http.StatusNotFound)
return
}
qty, err := strconv.Atoi(q.Get("qty"))
if err != nil || qty < 1 || qty > 10 {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprint(w, "qty must be 1 to 10")
return
}
fmt.Fprintf(w, "charged %d", qty*price)
}
// send writes an HTTP request by hand, the way any program could, and returns "status body".
func send(addr, path string) string {
conn, err := net.Dial("tcp", addr)
if err != nil {
panic(err)
}
defer conn.Close()
fmt.Fprintf(conn, "GET %s HTTP/1.1\r\nHost: shop\r\nConnection: close\r\n\r\n", path)
reply := bufio.NewReader(conn)
status, _ := reply.ReadString('\n') // "HTTP/1.1 200 OK"
for { // skip the headers: they end at the first empty line
line, err := reply.ReadString('\n')
if line == "\r\n" || err != nil {
break
}
}
body, _ := reply.ReadString(0)
return strings.Fields(status)[1] + " " + strings.TrimSpace(body)
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/trust", trust)
mux.HandleFunc("/checkout", checkout)
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
panic(err)
}
go http.Serve(ln, mux)
addr := ln.Addr().String()
fmt.Println("a book costs 500 taka; that price is stored on the server")
fmt.Printf("%-6s %-22s %-20s %s\n", "sender", "asks for", "/trust", "/checkout")
for _, c := range [][2]string{
{"app", "item=book&qty=2&price=500"},
{"curl", "item=book&qty=2&price=1"},
{"curl", "item=book&qty=-3&price=500"},
} {
fmt.Printf("%-6s %-22s %-20s %s\n", c[0], strings.TrimPrefix(c[1], "item=book&"), send(addr, "/trust?"+c[1]), send(addr, "/checkout?"+c[1]))
}
}const http = require("node:http");
const net = require("node:net");
// The price lives on the server. The client never gets to decide it.
const prices = { book: 500 };
const handlers = {
// /trust believes whatever the client sent, price included.
"/trust": (q) => [200, `charged ${Number(q.get("qty")) * Number(q.get("price"))}`],
// /checkout takes only what the user chose, and decides everything else itself.
"/checkout": (q) => {
const price = prices[q.get("item")];
if (price === undefined) return [404, "no such item"];
const qty = Number(q.get("qty"));
if (!Number.isInteger(qty) || qty < 1 || qty > 10) return [400, "qty must be 1 to 10"];
return [200, `charged ${qty * price}`];
},
};
const server = http.createServer((req, res) => {
const url = new URL(req.url, "http://shop");
const [status, body] = handlers[url.pathname]?.(url.searchParams) ?? [404, "not found"];
res.writeHead(status, { "Content-Length": Buffer.byteLength(body) }).end(body);
});
// send writes an HTTP request by hand, the way any program could, and resolves to "status body".
const send = (port, path) =>
new Promise((resolve, reject) => {
const conn = net.connect(port, "127.0.0.1", () =>
conn.write(`GET ${path} HTTP/1.1\r\nHost: shop\r\nConnection: close\r\n\r\n`),
);
let reply = "";
conn.on("data", (d) => (reply += d));
conn.on("end", () => {
const [head, body] = reply.split("\r\n\r\n"); // the headers end at the first empty line
resolve(`${head.split(" ")[1]} ${body.trim()}`);
});
conn.on("error", reject);
});
server.listen(0, "127.0.0.1", async () => {
const { port } = server.address();
console.log("a book costs 500 taka; that price is stored on the server");
console.log(`${"sender".padEnd(6)} ${"asks for".padEnd(22)} ${"/trust".padEnd(20)} /checkout`);
for (const [who, query] of [
["app", "item=book&qty=2&price=500"],
["curl", "item=book&qty=2&price=1"],
["curl", "item=book&qty=-3&price=500"],
]) {
const trusted = await send(port, `/trust?${query}`);
const checked = await send(port, `/checkout?${query}`);
console.log(`${who.padEnd(6)} ${query.replace("item=book&", "").padEnd(22)} ${trusted.padEnd(20)} ${checked}`);
}
server.close();
});use std::collections::HashMap;
use std::io::{BufRead, BufReader, Read, Write};
use std::net::{TcpListener, TcpStream};
use std::thread;
// The price lives on the server. The client never gets to decide it.
fn price_of(item: &str) -> Option<i64> {
match item {
"book" => Some(500),
_ => None,
}
}
// /trust believes whatever the client sent, price included.
fn trust(q: &HashMap<&str, &str>) -> (u16, String) {
let qty: i64 = q.get("qty").and_then(|v| v.parse().ok()).unwrap_or(0);
let price: i64 = q.get("price").and_then(|v| v.parse().ok()).unwrap_or(0);
(200, format!("charged {}", qty * price))
}
// /checkout takes only what the user chose, and decides everything else itself.
fn checkout(q: &HashMap<&str, &str>) -> (u16, String) {
let Some(price) = q.get("item").and_then(|i| price_of(i)) else {
return (404, "no such item".into());
};
let qty: i64 = q.get("qty").and_then(|v| v.parse().ok()).unwrap_or(0);
if !(1..=10).contains(&qty) {
return (400, "qty must be 1 to 10".into());
}
(200, format!("charged {}", qty * price))
}
// Reads the request line ("GET /trust?qty=2 HTTP/1.1"), routes it, and answers.
fn handle(conn: TcpStream) {
let mut reader = BufReader::new(&conn);
let mut line = String::new();
reader.read_line(&mut line).unwrap();
let target = line.split_whitespace().nth(1).unwrap_or("/");
let (path, query) = target.split_once('?').unwrap_or((target, ""));
let q: HashMap<&str, &str> = query.split('&').filter_map(|kv| kv.split_once('=')).collect();
let (status, body) = match path {
"/trust" => trust(&q),
"/checkout" => checkout(&q),
_ => (404, "not found".into()),
};
let mut header = String::new();
while reader.read_line(&mut header).unwrap_or(0) > 2 {
header.clear(); // skip the headers: they end at the first empty line
}
let reason = match status {
200 => "OK",
400 => "Bad Request",
_ => "Not Found",
};
let mut out = &conn;
write!(out, "HTTP/1.1 {status} {reason}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len()).unwrap();
}
// Writes an HTTP request by hand, the way any program could, and returns "status body".
fn send(addr: &str, path: &str) -> String {
let mut conn = TcpStream::connect(addr).unwrap();
write!(conn, "GET {path} HTTP/1.1\r\nHost: shop\r\nConnection: close\r\n\r\n").unwrap();
let mut reply = String::new();
conn.read_to_string(&mut reply).unwrap();
let (head, body) = reply.split_once("\r\n\r\n").unwrap(); // the headers end at the first empty line
format!("{} {}", head.split_whitespace().nth(1).unwrap(), body.trim())
}
fn main() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap().to_string();
thread::spawn(move || {
for conn in listener.incoming() {
handle(conn.unwrap());
}
});
println!("a book costs 500 taka; that price is stored on the server");
println!("{:<6} {:<22} {:<20} /checkout", "sender", "asks for", "/trust");
for (who, query) in [
("app", "item=book&qty=2&price=500"),
("curl", "item=book&qty=2&price=1"),
("curl", "item=book&qty=-3&price=500"),
] {
let trusted = send(&addr, &format!("/trust?{query}"));
let checked = send(&addr, &format!("/checkout?{query}"));
println!("{:<6} {:<22} {:<20} {}", who, query.replace("item=book&", ""), trusted, checked);
}
}import socket, threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import parse_qs, urlsplit
# The price lives on the server. The client never gets to decide it.
PRICES = {"book": 500}
def trust(q):
"""/trust believes whatever the client sent, price included."""
return 200, f"charged {int(q['qty']) * int(q['price'])}"
def checkout(q):
"""/checkout takes only what the user chose, and decides everything else itself."""
price = PRICES.get(q.get("item"))
if price is None:
return 404, "no such item"
try:
qty = int(q.get("qty", ""))
except ValueError:
qty = 0
if not 1 <= qty <= 10:
return 400, "qty must be 1 to 10"
return 200, f"charged {qty * price}"
class Shop(BaseHTTPRequestHandler):
def do_GET(self):
url = urlsplit(self.path)
q = {k: v[0] for k, v in parse_qs(url.query).items()}
handler = {"/trust": trust, "/checkout": checkout}.get(url.path)
status, body = handler(q) if handler else (404, "not found")
self.send_response(status)
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body.encode())
def log_message(self, *args): # keep the output quiet
pass
def send(port, path):
"""Writes an HTTP request by hand, the way any program could, and returns "status body"."""
with socket.create_connection(("127.0.0.1", port)) as conn:
conn.sendall(f"GET {path} HTTP/1.1\r\nHost: shop\r\nConnection: close\r\n\r\n".encode())
reply = b""
while chunk := conn.recv(4096):
reply += chunk
head, body = reply.decode().split("\r\n\r\n", 1) # the headers end at the first empty line
return f"{head.split()[1]} {body.strip()}"
server = ThreadingHTTPServer(("127.0.0.1", 0), Shop)
threading.Thread(target=server.serve_forever, daemon=True).start()
port = server.server_port
print("a book costs 500 taka; that price is stored on the server")
print(f"{'sender':<6} {'asks for':<22} {'/trust':<20} /checkout")
for who, query in [
("app", "item=book&qty=2&price=500"),
("curl", "item=book&qty=2&price=1"),
("curl", "item=book&qty=-3&price=500"),
]:
trusted = send(port, f"/trust?{query}")
checked = send(port, f"/checkout?{query}")
print(f"{who:<6} {query.replace('item=book&', ''):<22} {trusted:<20} {checked}")import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
public class Main {
// The price lives on the server. The client never gets to decide it.
static final Map<String, Integer> PRICES = Map.of("book", 500);
record Reply(int status, String body) {}
static Map<String, String> query(HttpExchange ex) {
Map<String, String> q = new HashMap<>();
String raw = ex.getRequestURI().getQuery();
if (raw != null) for (String kv : raw.split("&")) {
String[] p = kv.split("=", 2);
if (p.length == 2) q.put(p[0], p[1]);
}
return q;
}
static int number(String s) {
try { return Integer.parseInt(s); } catch (NumberFormatException e) { return 0; }
}
// /trust believes whatever the client sent, price included.
static Reply trust(Map<String, String> q) {
return new Reply(200, "charged " + number(q.get("qty")) * number(q.get("price")));
}
// /checkout takes only what the user chose, and decides everything else itself.
static Reply checkout(Map<String, String> q) {
Integer price = PRICES.get(q.get("item"));
if (price == null) return new Reply(404, "no such item");
int qty = number(q.get("qty"));
if (qty < 1 || qty > 10) return new Reply(400, "qty must be 1 to 10");
return new Reply(200, "charged " + qty * price);
}
static void route(HttpServer server, String path, java.util.function.Function<Map<String, String>, Reply> fn) {
server.createContext(path, ex -> {
Reply r = fn.apply(query(ex));
byte[] body = r.body().getBytes(StandardCharsets.UTF_8);
ex.sendResponseHeaders(r.status(), body.length);
try (OutputStream out = ex.getResponseBody()) { out.write(body); }
});
}
// Writes an HTTP request by hand, the way any program could, and returns "status body".
static String send(int port, String path) throws Exception {
try (Socket conn = new Socket("127.0.0.1", port)) {
conn.getOutputStream().write(("GET " + path + " HTTP/1.1\r\nHost: shop\r\nConnection: close\r\n\r\n").getBytes());
String reply = new String(conn.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
String[] parts = reply.split("\r\n\r\n", 2); // the headers end at the first empty line
return parts[0].split(" ")[1] + " " + parts[1].trim();
}
}
public static void main(String[] args) throws Exception {
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
route(server, "/trust", Main::trust);
route(server, "/checkout", Main::checkout);
server.start();
int port = server.getAddress().getPort();
System.out.println("a book costs 500 taka; that price is stored on the server");
System.out.printf("%-6s %-22s %-20s %s%n", "sender", "asks for", "/trust", "/checkout");
String[][] calls = {
{"app", "item=book&qty=2&price=500"},
{"curl", "item=book&qty=2&price=1"},
{"curl", "item=book&qty=-3&price=500"},
};
for (String[] c : calls) {
String trusted = send(port, "/trust?" + c[1]);
String checked = send(port, "/checkout?" + c[1]);
System.out.printf("%-6s %-22s %-20s %s%n", c[0], c[1].replace("item=book&", ""), trusted, checked);
}
server.stop(0);
}
}Output (পাঁচটা language এই একই):
a book costs 500 taka; that price is stored on the server
sender asks for /trust /checkout
app qty=2&price=500 200 charged 1000 200 charged 1000
curl qty=2&price=1 200 charged 2 200 charged 1000
curl qty=-3&price=500 200 charged -1500 400 qty must be 1 to 10
Output টা পড়ি:
- app, qty=2&price=500: দুটো endpoint ই 1000 নেয়। সৎ client এর সাথে দুটো design আলাদা করে চেনাই যায় না। এজন্যই এই bug ship করে ফেলা এত সহজ।
- curl, price=1:
/trustনেয় 2 টাকা।/checkoutনেয় 1000, কারণ সে দামটা কখনো পড়েইনি। - curl, qty=-3:
/trustনেয় −1500, যেটা আসল payment flow এ একটা refund হয়ে যেতে পারে।/checkoutউত্তর দেয়400: quantity এর rule server এ থাকে, তাই request যেই পাঠাক, ওটা চলবেই। - খেয়াল করো, কোন request কে পাঠিয়েছে server কে সেটা জানতেই হয়নি। Client থেকে শুধু পছন্দ নিয়ে আর বাকি সব নিজে হিসাব করে সে নিরাপদ থেকেছে।
আসল engineering
- Client কে কখনো বিশ্বাস করবে না। Backend security এর প্রথম নিয়ম এটা, আর এর মধ্যে পড়ে দাম, user id, role, file এর নাম আর size। Client validate করলেও server এ validate করো। (Module 5 আর Module 9।)
- Fact না, id পাঠাও। ভালো client পাঠায়
item=bookবাproductId=42; নাম, দাম আর stock server খুঁজে নেয়। যে clientpriceবাisAdmin=trueপাঠায়, তাকে দিয়ে মিথ্যা বলানো খুব সহজ। - যা গুরুত্বপূর্ণ, তার জন্য পাতলা client, মোটা server। Team গুলো টাকা, permission আর shared data রাখে server এ, আর client কে রাখে দ্রুত আর সহজ। কিছু logic ইচ্ছা করেই দুই জায়গায় থাকে: client check করে গতির জন্য, server check করে সত্যের জন্য।
- পুরনো client চিরকাল বেঁচে থাকে। User রা app update করে না। বছর আগে ship করা version এর request ও তোমার server কে উত্তর দিয়ে যেতে হবে, এজন্যই API এর version থাকে। (Module 3, lesson 15।)
- Mobile app গোপন কিছু না। App এ যা ship করো (key, লুকানো URL, "admin" flag) সব বের করে ফেলা যায়। আসল system secret রাখে server এ, আর client কে দেয় অল্প সময়ের token।
যখন ভাঙে
এর প্রতিটা real product এ ঘটেছে।
একটা failure বাছো
POST /orders {"item": 42, "price": 1}
order saved, charged 1 takaপড়ার আগে ভাবো
তোমার API শুধু তোমার নিজের mobile app ই ব্যবহার করে, আর API এর URL কোথাও public এ লেখা নেই। আপাতত server এ check না করলে কি চলে?
উত্তর দেখো
না। API এর URL app এর ভেতরেই আছে, আর app এর network traffic দেখে যে কেউ সেটা বের করে ফেলতে পারে, একটা free proxy tool দিয়ে মোটামুটি পাঁচ মিনিটে। "কেউ URL জানে না" কোনো সুরক্ষা না। Server কে ধরে নিতে হবে প্রতিটা request হাতে লেখা হতে পারে, কারণ কিছু তো হবেই।
নিজেকে check করো
1. এক বাক্যে, client আর server এর পার্থক্য কী?
উত্তর
Client চলে user এর device এ আর নিয়ন্ত্রণ করে user; server চলে তোমার নিয়ন্ত্রণের machine এ, আর shared data আর rule রাখে।
2. Client কেন price না পাঠিয়ে productId পাঠাবে?
উত্তর
কারণ client যা পাঠায় সব বদলানো যায়। Id শুধু একটা পছন্দ; আসল দাম server নিজের data থেকে খোঁজে, তাই id বদলালে শুধু অন্য product বাছা যায়, অন্য দাম না।
3. Server যদি সব check করেই, তাহলে client এ validate করার দরকার কী?
উত্তর
User এর জন্য। সাথে সাথে feedback পাওয়া একটা round trip এর চেয়ে দ্রুত আর আরামের, আর server এর কিছু অকাজের request বাঁচে। শুধু এটা security না।
4. দুটো জিনিসের নাম বলো যা কখনো client এ রাখা যাবে না।
উত্তর
যেমন: secret API key, password বা অন্য user দের data, আর দাম বা permission ঠিক করার ক্ষমতা।
তোমার challenge: ফুটোটা বন্ধ করো
উপরের program এ তৃতীয় একটা endpoint যোগ করো, /discount?item=book&qty=2&coupon=HALF।
- Server এর নিজের coupon list আছে:
HALFএ 50% ছাড়,TENএ 10% ছাড়। - অজানা coupon এর উত্তর
400 unknown coupon। - Client যেন discount এর পরিমাণ পাঠাতে না পারে, শুধু coupon এর code।
চালানোর আগে আন্দাজ করো /discount?item=book&qty=2&coupon=HALF&discount=100 কী ফেরত দেওয়া উচিত।
সফল হয়েছো যখন তোমার হাতে লেখা request গুলো HALF এর জন্য 200 charged 500, TEN এর জন্য 200 charged 900, FREE এর জন্য 400 unknown coupon print করে, আর বাড়তি discount=100 কিছুই বদলায় না।
Mental model
Client হলো user এর program, user এর device এ, user এর নিয়ন্ত্রণে; server তোমার। লাইন পার হয়ে যা আসে সব দাবি, fact না। Client দেখাক আর প্রস্তাব দিক; server সিদ্ধান্ত নিক, হিসাব করুক আর মনে রাখুক। Client এ check করো গতির জন্য, server এ সত্যের জন্য।
Station stamp
Chapter শেষ করো
পড়া শেষ হলে Complete চাপো, progress save হবে। XP শুধু Pro তে — Pro থাকলে এই chapter এ +10 XP পেতে।
Pro নিয়ে XP পাও →