คุณเคยสร้างเว็บเซิร์ฟเวอร์ของคุณเองด้วย Ruby หรือไม่
เรามีเซิร์ฟเวอร์มากมาย เช่น:
- พูม่า
- บาง
- ยูนิคอร์น
แต่ฉันคิดว่านี่เป็นแบบฝึกหัดการเรียนรู้ที่ยอดเยี่ยม หากคุณต้องการทราบว่าเว็บเซิร์ฟเวอร์ทำงานอย่างไร
ในบทความนี้ คุณจะได้เรียนรู้วิธีการทำเช่นนี้
ทีละขั้นตอน!
ขั้นตอนที่ 1:การฟังสำหรับการเชื่อมต่อ
เราจะเริ่มต้นที่ไหน
สิ่งแรกที่เราต้องการคือการฟังการเชื่อมต่อใหม่บนพอร์ต TCP 80
ฉันเขียนโพสต์เกี่ยวกับการเขียนโปรแกรมเครือข่ายใน Ruby แล้ว ฉันจะไม่อธิบายวิธีการทำงานที่นี่
ฉันจะให้รหัสกับคุณ :
require 'socket' server = TCPServer.new('localhost', 80) loop { client = server.accept request = client.readpartial(2048) puts request }
เมื่อคุณเรียกใช้รหัสนี้ คุณจะมีเซิร์ฟเวอร์ที่ยอมรับการเชื่อมต่อบนพอร์ต 80 ซึ่งยังไม่ช่วยอะไรมากนัก แต่จะช่วยให้คุณเห็นว่าคำขอที่เข้ามานั้นเป็นอย่างไร
หมายเหตุ :ในการใช้พอร์ต 80 ในระบบ Linux/Mac คุณจะต้องมีสิทธิ์รูท คุณสามารถใช้พอร์ตอื่นที่สูงกว่า 1024 แทนได้ ฉันชอบ 8080 🙂
วิธีง่ายๆ ในการสร้างคำขอคือใช้เบราว์เซอร์ของคุณหรือบางอย่าง เช่น curl
.
เมื่อคุณทำเช่นนั้น คุณจะเห็นสิ่งนี้พิมพ์อยู่ในเซิร์ฟเวอร์ของคุณ:
GET / HTTP/1.1 Host: localhost User-Agent: curl/7.49.1 Accept: */*
นี่คือคำขอ HTTP HTTP เป็นโปรโตคอลข้อความธรรมดาที่ใช้สำหรับการสื่อสารระหว่างเว็บเบราว์เซอร์และเว็บเซิร์ฟเวอร์
สามารถดูข้อกำหนดโปรโตคอลอย่างเป็นทางการได้ที่นี่:https://tools.ietf.org/html/rfc7230
ขั้นตอนที่ 2:การแยกวิเคราะห์คำขอ
ตอนนี้ เราต้องแยกย่อยคำขอออกเป็นส่วนประกอบเล็กๆ ที่เซิร์ฟเวอร์ของเราสามารถเข้าใจได้
ในการทำเช่นนั้น เราสามารถสร้าง parser ของเราเองหรือใช้อันที่มีอยู่แล้ว เรากำลังจะสร้างของเราเอง ดังนั้นเราต้องเข้าใจว่าส่วนต่างๆ ของคำขอหมายถึงอะไร
ภาพนี้น่าจะช่วยได้ :
รับคำขอ
ส่วนหัวใช้สำหรับสิ่งต่างๆ เช่น การแคชเบราว์เซอร์ การโฮสต์เสมือน และการบีบอัดข้อมูล แต่สำหรับการใช้งานพื้นฐาน เราสามารถเพิกเฉยได้และยังคงมีเซิร์ฟเวอร์ที่ใช้งานได้
ในการสร้างตัวแยกวิเคราะห์ HTTP อย่างง่าย เราสามารถใช้ประโยชน์จากข้อเท็จจริงที่ว่าข้อมูลคำขอถูกแยกออกด้วยการขึ้นบรรทัดใหม่ (\r\n
). เราจะไม่ทำการตรวจสอบข้อผิดพลาดหรือความถูกต้องเพื่อให้ทุกอย่างง่ายขึ้น
นี่คือรหัสที่ฉันใช้:
def parse(request) method, path, version = request.lines[0].split { path: path, method: method, headers: parse_headers(request) } end def parse_headers(request) headers = {} request.lines[1..-1].each do |line| return headers if line == "\r\n" header, value = line.split header = normalize(header) headers[header] = value end def normalize(header) header.gsub(":", "").downcase.to_sym end end
การดำเนินการนี้จะส่งคืนแฮชพร้อมข้อมูลคำขอที่แยกวิเคราะห์ ตอนนี้เรามีคำขอของเราในรูปแบบที่ใช้งานได้ เราสามารถสร้างการตอบสนองให้กับลูกค้าได้
ขั้นตอนที่ 3:การเตรียมและส่งคำตอบ
ในการสร้างการตอบสนอง เราต้องดูว่าทรัพยากรที่ร้องขอนั้นพร้อมใช้งานหรือไม่ กล่าวอีกนัยหนึ่งเราต้องตรวจสอบว่าไฟล์นั้นมีอยู่หรือไม่
นี่คือรหัสที่ฉันเขียนสำหรับการทำเช่นนั้น:
SERVER_ROOT = "/tmp/web-server/" def prepare_response(request) if request.fetch(:path) == "/" respond_with(SERVER_ROOT + "index.html") else respond_with(SERVER_ROOT + request.fetch(:path)) end end def respond_with(path) if File.exists?(path) send_ok_response(File.binread(path)) else send_file_not_found end end
มีสองสิ่งที่เกิดขึ้นที่นี่ :
- ขั้นแรก หากกำหนดเส้นทางเป็น
/
เราคิดว่าไฟล์ที่เราต้องการคือindex.html
. - ประการที่สอง หากพบไฟล์ที่ร้องขอ เราจะส่งเนื้อหาไฟล์ด้วยการตอบกลับว่าตกลง
แต่ถ้าไม่พบไฟล์ เราจะส่ง 404 Not Found
. ทั่วไป ตอบกลับ
ตารางรหัสตอบกลับ HTTP ที่พบบ่อยที่สุด
สำหรับการอ้างอิง
รหัส | คำอธิบาย |
---|---|
200 | ตกลง |
301 | ย้ายถาวร |
302 | พบ |
304 | ไม่ได้แก้ไข |
400 | คำขอไม่ถูกต้อง |
401 | ไม่ได้รับอนุญาต |
403 | ต้องห้าม |
404 | ไม่พบ |
500 | ข้อผิดพลาดของเซิร์ฟเวอร์ภายใน |
502 | เกตเวย์ไม่ถูกต้อง |
ระดับและวิธีการตอบสนอง
นี่คือวิธีการ “ส่ง” ที่ใช้ในตัวอย่างที่แล้ว:
def send_ok_response(data) Response.new(code: 200, data: data) end def send_file_not_found Response.new(code: 404) end
และนี่คือ Response
คลาส:
class Response attr_reader :code def initialize(code:, data: "") @response = "HTTP/1.1 #{code}\r\n" + "Content-Length: #{data.size}\r\n" + "\r\n" + "#{data}\r\n" @code = code end def send(client) client.write(@response) end end
การตอบกลับสร้างขึ้นจากเทมเพลตและการแก้ไขสตริงบางส่วน
ณ จุดนี้เราแค่ต้องผูกทุกอย่างเข้าด้วยกันใน loop
ที่ยอมรับการเชื่อมต่อของเรา แล้วเราก็ควรมีเซิร์ฟเวอร์ที่ใช้งานได้
loop { client = server.accept request = client.readpartial(2048) request = RequestParser.new.parse(request) response = ResponsePreparer.new.prepare(request) puts "#{client.peeraddr[3]} #{request.fetch(:path)} - #{response.code}" response.send(client) client.close }
ลองเพิ่มไฟล์ HTML บางส่วนภายใต้ SERVER_ROOT
ไดเร็กทอรีและคุณควรจะสามารถโหลดได้จากเบราว์เซอร์ของคุณ การดำเนินการนี้จะให้บริการเนื้อหาแบบคงที่อื่นๆ รวมถึงรูปภาพด้วย
แน่นอนว่าเว็บเซิร์ฟเวอร์จริงๆ มีฟีเจอร์อีกมากมายที่เราไม่ได้กล่าวถึง
นี่คือรายการของ บางส่วน ของคุณสมบัติที่ขาดหายไป คุณจึงนำไปปรับใช้เป็นแบบฝึกหัดได้ (การฝึกฝนคือต้นกำเนิดของทักษะ!):
- โฮสติ้งเสมือน
- ประเภทละครใบ้
- การบีบอัดข้อมูล
- การควบคุมการเข้าถึง
- มัลติเธรด
- ขอตรวจสอบความถูกต้อง
- การแยกวิเคราะห์สตริงข้อความค้นหา
- การแยกวิเคราะห์เนื้อหา POST
- การแคชเบราว์เซอร์ (รหัสตอบกลับ 304)
- เปลี่ยนเส้นทาง
บทเรียนด้านความปลอดภัย
การรับข้อมูลจากผู้ใช้และการทำอะไรกับมันเป็นสิ่งที่อันตรายเสมอ ในโครงการเว็บเซิร์ฟเวอร์เล็กๆ ของเรา อินพุตของผู้ใช้คือคำขอ HTTP
เราได้แนะนำช่องโหว่เล็กๆ น้อยๆ ที่เรียกว่า "การข้ามเส้นทาง" ผู้คนจะสามารถอ่านไฟล์ใดๆ ที่ผู้ใช้เว็บเซิร์ฟเวอร์ของเราสามารถเข้าถึงได้ แม้ว่าจะอยู่นอก SERVER_ROOT
ของเรา ไดเรกทอรี
นี่คือสายที่รับผิดชอบสำหรับปัญหานี้:
File.binread(path)
คุณสามารถลองใช้ปัญหานี้ด้วยตัวเองเพื่อดูการทำงานจริง คุณจะต้องสร้างคำขอ HTTP "ด้วยตนเอง" เนื่องจากไคลเอ็นต์ HTTP ส่วนใหญ่ (รวมถึง curl
) จะประมวลผล URL ของคุณล่วงหน้าและลบส่วนที่ก่อให้เกิดช่องโหว่
เครื่องมือหนึ่งที่คุณสามารถใช้ได้คือ netcat
นี่คือช่องโหว่ที่เป็นไปได้:
$ nc localhost 8080 GET ../../etc/passwd HTTP/1.1
สิ่งนี้จะส่งคืนเนื้อหาของ /etc/passwd
ไฟล์หากคุณใช้ระบบที่ใช้ Unix สาเหตุที่ใช้งานได้เพราะจุดคู่ (..
) อนุญาตให้คุณขึ้นหนึ่งไดเร็กทอรี ดังนั้นคุณกำลัง "หลบหนี" SERVER_ROOT
ไดเรกทอรี
ทางออกหนึ่งที่เป็นไปได้คือ “บีบอัด” จุดหลายจุดให้เป็นหนึ่งเดียว:
path.gsub!(/\.+/, ".")
เมื่อคิดถึงความปลอดภัย ให้สวม "หมวกแฮ็กเกอร์" เสมอ และพยายามหาวิธีที่จะทำลายโซลูชันของคุณ ตัวอย่างเช่น หากคุณเพิ่งทำ path.gsub!("..", ".")
คุณสามารถข้ามได้โดยใช้จุดสามจุด (...
)
เสร็จสิ้น &รหัสการทำงาน
ฉันรู้ว่าโค้ดมีอยู่ทั่วไปในโพสต์นี้ ดังนั้นหากคุณกำลังมองหาโค้ดที่ใช้งานได้สำเร็จ…
นี่คือลิงค์ :
https://gist.github.com/matugm/efe0a1c4fc53310f7ac93dcd1f041f6c#file-web-server-rb
สนุก!
สรุป
ในโพสต์นี้ คุณได้เรียนรู้วิธีฟังการเชื่อมต่อใหม่ คำขอ HTTP หน้าตาเป็นอย่างไร และวิธีแยกวิเคราะห์ คุณยังได้เรียนรู้วิธีสร้างการตอบกลับโดยใช้รหัสตอบกลับและเนื้อหาของไฟล์ที่จำเป็น (ถ้ามี)
และในที่สุด คุณได้เรียนรู้เกี่ยวกับช่องโหว่ "การข้ามเส้นทาง" และวิธีหลีกเลี่ยง
ฉันหวังว่าคุณจะสนุกกับโพสต์นี้และเรียนรู้สิ่งใหม่! อย่าลืมสมัครรับจดหมายข่าวของฉันในแบบฟอร์มด้านล่าง คุณจะได้ไม่พลาดโพสต์เดียว 🙂