33. Request dan formulir
Sejauh ini, request hanya memilih halaman. Sekarang pengunjung mengirim data kepadamu, di alamat atau di formulir. Bab ini membaca data itu, memeriksanya, dan menjawab dengan status yang tepat. Bab ini juga menunjukkan satu kesalahan yang tidak boleh dilakukan programmer web.
Semua input tidak bisa dipercaya
Semua isi request datang dari luar programmu. Pengunjung bisa
mengetik apa saja ke formulir, mengubah alamat dengan tangan, atau
mengirim request tanpa browser sama sekali, seperti yang sudah kamu
lakukan dengan curl.exe. Batasan di HTML-mu, seperti daftar
pilihan, tidak membatasi apa yang bisa sampai.
Jadi setiap handler mengikuti langkah yang sama:
- Baca nilainya. Nilai itu selalu berupa string.
- Periksa. Kalau salah, jawab dengan status error dan
return. - Kerjakan tugasnya.
Ini validasi dari Bab 20, dengan kode status sebagai ganti bertanya lagi.
Query string
Bagian URL sesudah ? adalah query
string. Isinya nama dan nilai:
http://localhost:8080/add?a=2&b=3
a bernilai 2, dan b bernilai
3, dengan & di antaranya.
r.URL.Query() memisahkannya, dan .Get membaca
satu nilai. Ini exercises/33/01-query-string.go:
// Chapter 33 example: reading values from the query string.
// Run with: go run exercises/33/01-query-string.go
package main
import (
"fmt"
"net/http"
"strconv"
)
func handleHello(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if name == "" {
http.Error(w, "Please give a name, as in /hello?name=Keiko", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
fmt.Fprintf(w, "Hello, %s!\n", name)
}
func handleAdd(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
a, err := strconv.Atoi(query.Get("a"))
if err != nil {
http.Error(w, "a must be a whole number.", http.StatusBadRequest)
return
}
b, err := strconv.Atoi(query.Get("b"))
if err != nil {
http.Error(w, "b must be a whole number.", http.StatusBadRequest)
return
}
fmt.Fprintf(w, "%d + %d = %d\n", a, b, a+b)
}
func main() {
http.HandleFunc("GET /hello", handleHello)
http.HandleFunc("GET /add", handleAdd)
fmt.Println("Server running at http://localhost:8080")
fmt.Println("Press Ctrl+C to stop it.")
err := http.ListenAndServe("localhost:8080", nil)
if err != nil {
fmt.Println("Server stopped:", err)
}
}Get mengembalikan string kosong untuk nama yang tidak
ada, sama seperti untuk nilai kosong, jadi pemeriksaan pada
name mencakup keduanya. Angka butuh
strconv.Atoi dan pemeriksaan error-nya, seperti di Bab 21,
dan teks yang buruk menjadi status 400. Pengunjung yang membuat
kesalahan, jadi kodenya 4xx. Hasilnya:
curl.exe -i "http://localhost:8080/hello?name=Keiko"
HTTP/1.1 200 OK
Hello, Keiko!
curl.exe -i http://localhost:8080/hello
HTTP/1.1 400 Bad Request
Please give a name, as in /hello?name=Keiko
curl.exe -i "http://localhost:8080/add?a=2&b=3"
HTTP/1.1 200 OK
2 + 3 = 5
curl.exe -i "http://localhost:8080/add?a=abc&b=3"
HTTP/1.1 400 Bad Request
a must be a whole number.
Di PowerShell, beri tanda kutip ganda pada URL yang memuat
&. Tanpa itu, PowerShell berhenti dengan error tentang
&, bahkan sebelum curl.exe berjalan.
Spasi tidak bisa muncul begitu saja di URL, jadi ditulis sebagai
%20 atau +:
/hello?name=Keiko%20Ito memberikan
Hello, Keiko Ito!. Get menerjemahkannya
untukmu. Browser mengodekan URL untukmu ketika kamu mengetiknya.
handleHello mengatur Content-Type menjadi
text/plain dengan sengaja. Namanya berasal dari pengunjung,
dan ini berarti jawabannya tidak boleh dibaca sebagai HTML. Bagian
tentang output yang tidak aman menjelaskan alasannya.
Jalankan exercises/33/01-query-string.go, dan buka
http://localhost:8080/add?a=9223372036854775807&b=1.
Jawabannya negatif. Atoi menerima kedua angka, dan jumlahnya tidak muat
dalam int. Bagaimana kamu menolak angka yang terlalu besar?
Nilai di path
Pola bisa punya wildcard bernama di dalam kurung
kurawal, yang cocok dengan satu bagian path. r.PathValue
membacanya. Ini exercises/33/02-path-values.go, bagian
handler dan di bawahnya:
func handlePlayer(w http.ResponseWriter, r *http.Request) {
name := r.PathValue("name")
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
fmt.Fprintf(w, "This is the page of %s.\n", name)
}
func handleSquare(w http.ResponseWriter, r *http.Request) {
n, err := strconv.Atoi(r.PathValue("n"))
if err != nil {
http.Error(w, "The path must end in a whole number.", http.StatusBadRequest)
return
}
if n < -1000 || n > 1000 {
http.Error(w, "The number must be from -1000 to 1000.", http.StatusBadRequest)
return
}
fmt.Fprintf(w, "%d squared is %d\n", n, n*n)
}
func main() {
http.HandleFunc("GET /players/{name}", handlePlayer)
http.HandleFunc("GET /square/{n}", handleSquare)Hasilnya:
curl.exe -i http://localhost:8080/players/Keiko
HTTP/1.1 200 OK
This is the page of Keiko.
curl.exe -i http://localhost:8080/players/a/b
HTTP/1.1 404 Not Found
404 page not found
curl.exe -i http://localhost:8080/square/12
HTTP/1.1 200 OK
12 squared is 144
curl.exe -i http://localhost:8080/square/abc
HTTP/1.1 400 Bad Request
The path must end in a whole number.
curl.exe -i http://localhost:8080/square/99999
HTTP/1.1 400 Bad Request
The number must be from -1000 to 1000.
Pemeriksaan rentang menghentikan overflow yang kamu lihat di kotak
Coba ini. {name} hanya cocok dengan satu bagian, jadi
/players/a/b tidak cocok, begitu juga
/players/.
Pakai path untuk hal yang mana yang kamu inginkan, dan query string untuk bagaimana kamu menginginkannya.
Formulir
Formulir HTML memungkinkan pengunjung mengetik nilai, dan mengubahnya menjadi request:
<form action="/message" method="post">
<p><label>Message: <input type="text" name="message"></label></p>
<p><button type="submit">Send</button></p>
</form>actionadalah path tujuan request.methodadalahgetataupost.namepada setiap input adalah nama nilainya. Pengunjung tidak pernah melihatnya, dan handlermu memintanya dengan nama ini.
GET atau POST
Metode mengatakan untuk apa request itu:
- GET meminta sesuatu, dan tidak mengubah apa-apa.
Formulir dengan
method="get"menaruh nilainya di query string, tempat nilai itu bisa dilihat, disimpan sebagai bookmark, dan dimuat ulang. Kotak pencarian adalah contoh yang baik. - POST mengirim sesuatu yang mengubah hal lain, seperti menambah pesan. Nilainya ada di body request. Browser bertanya dulu sebelum mengirimnya dua kali.
Browser dan program lain boleh mengulang GET kapan saja, dan boleh mengambil tautan sebelum kamu mengkliknya. Jadi apa pun yang mengubah data harus berupa POST.
Membaca formulir
r.FormValue("message") membaca nilai dari formulir. Dia
melihat ke body POST dan ke query string, jadi berlaku untuk keduanya.
Seperti Get, dia mengembalikan string kosong untuk nilai
yang tidak ada. Ini exercises/33/03-form-post.go:
// Chapter 33 example: a form, POST, validation, and a redirect.
// Run with: go run exercises/33/03-form-post.go
package main
import (
"fmt"
"net/http"
"strings"
"unicode/utf8"
)
const maxMessageLength = 100
const formPage = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Send a message</title>
</head>
<body>
<h1>Send a message</h1>
<form action="/message" method="post">
<p><label>Message: <input type="text" name="message"></label></p>
<p><button type="submit">Send</button></p>
</form>
</body>
</html>
`
const thanksPage = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Thank you</title>
</head>
<body>
<h1>Thank you</h1>
<p>The message arrived. Look at the terminal.</p>
<p><a href="/">Send another</a></p>
</body>
</html>
`
func handleForm(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprint(w, formPage)
}
func handleMessage(w http.ResponseWriter, r *http.Request) {
message := strings.TrimSpace(r.FormValue("message"))
if message == "" {
http.Error(w, "The message is empty.", http.StatusBadRequest)
return
}
if utf8.RuneCountInString(message) > maxMessageLength {
http.Error(w, "The message is too long.", http.StatusBadRequest)
return
}
fmt.Printf("Received: %q\n", message)
http.Redirect(w, r, "/thanks", http.StatusSeeOther)
}
func handleThanks(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprint(w, thanksPage)
}
func main() {
http.HandleFunc("GET /{$}", handleForm)
http.HandleFunc("POST /message", handleMessage)
http.HandleFunc("GET /thanks", handleThanks)
fmt.Println("Server running at http://localhost:8080")
fmt.Println("Press Ctrl+C to stop it.")
err := http.ListenAndServe("localhost:8080", nil)
if err != nil {
fmt.Println("Server stopped:", err)
}
}Jalankan, dan buka http://localhost:8080. Ketik pesan
dan kirim. Browser berakhir di halaman terima kasih, dan terminal
menampilkan:
Received: "Hello there"
Handler itu memakai %q dengan sengaja. Teksnya berasal
dari pengunjung dan mungkin memuat akhir baris, dan %q
menjaganya tetap satu baris, di dalam tanda kutip.
handleMessage mengikuti ketiga langkah tadi.
strings.TrimSpace lebih dulu, supaya pesan yang hanya
berisi spasi dihitung kosong. Lalu dua pemeriksaan, masing-masing
diakhiri return. Untuk menghitung karakter dan bukan byte,
dia memakai utf8.RuneCountInString dari Bab 12. Baru
sesudah itu dia mengerjakan tugasnya, yang di sini berupa satu baris
pencetakan.
curl.exe -d mengirim POST dengan nilai yang kamu
berikan. Inilah tiga jalur melalui handler itu. Pesan panjang di bawah
adalah hasil nyata dengan 101 karakter, yang dipendekkan di sini:
curl.exe -i -d "message=Hello there" http://localhost:8080/message
HTTP/1.1 303 See Other
Location: /thanks
curl.exe -i -d "message=" http://localhost:8080/message
HTTP/1.1 400 Bad Request
The message is empty.
curl.exe -i -d "message=xxxxx...xxxxx" http://localhost:8080/message
HTTP/1.1 400 Bad Request
The message is too long.
Dan metode yang salah:
curl.exe -i http://localhost:8080/message
HTTP/1.1 405 Method Not Allowed
Allow: POST
Method Not Allowed
Go juga membatasi berapa banyak yang dibaca. r.FormValue
membaca paling banyak 10 MB dari body formulir biasa. Body yang lebih
besar ditolak: r.FormValue mengembalikan string kosong,
jadi handler ini menjawab The message is empty. Itu cukup
untuk contoh-contoh ini. Batas dalam pemeriksaanmu sendirilah yang
dilihat pengunjung.
Redirect sesudah POST
POST yang berhasil tidak mengirim halaman. Dia mengirim redirect, yang menyuruh browser pergi ke tempat lain:
http.Redirect(w, r, "/thanks", http.StatusSeeOther)Status 303, See Other, dan header
Location: /thanks membuat browser mengirim
GET /thanks yang baru. Mengapa tidak langsung menjawab
dengan halaman terima kasih? Karena tombol muat ulang. Memuat ulang
mengulang request terakhir. Kalau request terakhir adalah POST, memuat
ulang mengirim pesan itu lagi. Sesudah redirect, request terakhir adalah
GET yang tidak berbahaya, dan memuat ulang hanya menampilkan halaman
terima kasih lagi.
Pola ini, POST lalu redirect lalu GET, dipakai pada hampir semua
formulir di web. Pakai status 303, dan return kalau masih
ada kode sesudah pemanggilan itu.
Di exercises/33/03-form-post.go, ganti baris
http.Redirect dengan
fmt.Fprint(w, thanksPage). Mulai ulang, kirim pesan, lalu
muat ulang halamannya. Browser bertanya apakah formulirnya perlu dikirim
lagi. Kalau kamu setuju, terminal menampilkan pesan itu untuk kedua
kalinya. Kembalikan redirect-nya sesudah itu.
Lupa return
http.Error menulis response, tetapi tidak mengakhiri
fungsimu. Hilangkan return dari handleHello,
dan kodenya berlanjut. Request tanpa nama mendapat ini:
Please give a name, as in /hello?name=Keiko
Hello, !
Statusnya tetap 400, tetapi sisa handler berjalan dengan data yang
buruk. Di handler yang menyimpan data, data itu akan tersimpan. Setiap
pemeriksaan error di handler diakhiri dengan return.
Mencetak input ke HTML itu tidak aman
Inilah kesalahan yang dijanjikan bab ini. Kelihatannya tidak
berbahaya. Ini exercises/33/04-unsafe-echo.go, dengan
komentarnya, dan dia sengaja dibuat tidak aman:
func handleEcho(w http.ResponseWriter, r *http.Request) {
text := r.URL.Query().Get("text")
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprintf(w, "<p>You said: %s</p>\n", text)
}Dengan ?text=hello, halamannya berbunyi
You said: hello. Sekarang minta ini, dengan sepotong HTML
sebagai teksnya. -G menaruh nilainya di query string, dan
--data-urlencode mengodekannya:
curl.exe -i -G --data-urlencode "text=<script>alert('hello')</script>" http://localhost:8080/echo
Body response-nya:
<p>You said: <script>alert('hello')</script></p>
Programmu sudah mengirim tag <script>, dan browser
menjalankan apa yang ada di dalamnya. Kamu bisa mencobanya di browser,
dengan membuka alamat di bawah. Sebuah kotak kecil seharusnya
muncul.
http://localhost:8080/echo?text=<script>alert('hello')</script>
Dengan kata sederhana: pengunjung telah menulis sebagian halamanmu. Kotak itu tidak berbahaya, tetapi skrip bisa melakukan apa pun yang bisa dilakukan halaman itu. Lebih buruk lagi, teksnya tidak harus diketik oleh orang yang melihatnya. Seseorang bisa mengirim tautan berisi skrip kepada temannya. Atau, begitu server menyimpan pesan, pesan satu pengunjung bisa berjalan di browser semua pengunjung berikutnya.
Ini dikenal sebagai cross-site scripting.
Perbaikannya adalah mengubah < menjadi
< dan karakter khusus lainnya menjadi kode serupa,
supaya browser menampilkannya sebagai teks. Melakukan itu dengan tangan,
di setiap handler, mudah terlupa, jadi kamu tidak akan melakukannya
dengan tangan. Bab 34 memakai package yang melakukannya setiap saat.
Sampai saat itu, jawab dengan text/plain ketika kamu
mencetak input pengunjung, seperti yang dilakukan
handleHello. Browser tidak menjalankan apa pun di dalam
teks biasa.
Jangan pernah menyalin handleEcho ke program sungguhan.
Hal yang sama berlaku untuk membangun HTML dengan
fmt.Fprintf dan nilai apa pun dari request.
PR
Starter PR ada di exercises/33/homework/. Masing-masing
bisa langsung dijalankan dan menjawab dengan satu baris pengganti.
Soal 1 — Sapaan dari query string
Buka exercises/33/homework/01-greet.go.
GET /greet?name=Keiko menjawab dengan teks biasa
Good morning, Keiko. Nama yang tidak ada atau kosong
menjawab dengan status 400 dan pesan pendek. Atur
Content-Type menjadi text/plain, karena
namanya berasal dari pengunjung.
Soal 2 — Kelipatan dua dari path
Buka exercises/33/homework/02-double.go.
GET /double/21 menjawab dengan
21 doubled is 42. Pakai r.PathValue dan
strconv.Atoi. Nilai yang bukan bilangan bulat, atau tidak
berada di antara -1000000 sampai 1000000,
menjawab dengan status 400.
Soal 3 — Formulir penilaian
Buka exercises/33/homework/03-rating-form.go.
Formulirnya sudah selesai. Tulis handleRating: baca
rating dengan r.FormValue, ubah dengan
strconv.Atoi, dan jawab dengan status 400 kalau bukan
bilangan bulat dari 1 sampai 5. Kalau tidak, cetak nilainya ke terminal,
dan redirect ke /thanks dengan status 303.
Tantangan — Kalkulator
Buka exercises/33/homework/04-calculator.go. Tulis
formulir dengan method="get" dan
action="/calc", dengan input teks a dan
b, dan <select> bernama op
dengan pilihan add, sub, mul, dan
div. Tulis handleCalc, yang menjawab dengan
teks seperti 6 div 3 = 2.
Ini semua menjawab dengan status 400 dan pesan yang berguna:
a atau b bukan bilangan bulat, a
atau b tidak berada di antara -1000000 sampai
1000000, op bukan salah satu dari keempatnya,
dan pembagian dengan 0. Pikirkan mengapa formulir ini memakai GET dan
bukan POST.
Stuck atau sudah selesai? Buka halaman solusi PR.