Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

สร้าง Custom Ruby Web Server ตั้งแต่ศูนย์: คู่มือทีละขั้นตอน

เคยสร้าง web server ของตัวเองด้วย Ruby ไหม?

เรามี server หลายตัวแล้ว เช่น:

  • Puma
  • Thin
  • Unicorn

แต่ผมคิดว่า นี่คือ exercise เรียนรู้ที่ยอดเยี่ยม ถ้าอยากรู้ว่า web server 簡単ทำงานอย่างไร

ในบทความนี้ คุณจะได้เรียนรู้วิธีทำ ทีละขั้นตอน!

ขั้นตอน 1: รอรับ Connection

เริ่มที่ไหนดี?

สิ่งแรกคือต้อง listen สำหรับ connection ใหม่บน TCP port 80

ผมเขียน post เกี่ยว network programming ใน Ruby ไปแล้ว จึงไม่解釋ที่นี่

แค่ให้โค้ด:

require 'socket'
server = TCPServer.new('localhost', 80)
loop {
  client = server.accept
  request = client.readpartial(2048)
  puts request
}

รันโค้ดนี้ จะได้ server ที่รับ connection บน port 80 ยังไม่ทำอะไรมาก แต่มอง request ที่เข้าไปได้

หมายเหตุ: ใช้ port 80 บน Linux/Mac ต้องมีสิทธิ์ root ถ้าไม่มี ใช้ port อื่นเหนือ 1024 ผมชอบ 8080 🙂

วิธีง่ายสร้าง request คือใช้ browser หรือ curl

ทำแล้วจะเห็น output ใน server:

GET / HTTP/1.1
Host: localhost
User-Agent: curl/7.49.1
Accept: */*

นี่คือ HTTP request HTTP เป็น plain-text protocol ใช้สื่อสารระหว่าง browser และ web server

Spec อยู่ที่: https://tools.ietf.org/html/rfc7230

ขั้นตอน 2: Parse Request

ต้องแยก request เป็นส่วนๆ ที่ server เข้าใจ

สร้าง parser เอง หรือใช้ที่มีอยู่ เราจะสร้างเอง จึงต้องเข้าใจส่วนต่างๆ ของ request

รูปนี้ช่วยให้เข้าใจ:

สร้าง Custom Ruby Web Server ตั้งแต่ศูนย์: คู่มือทีละขั้นตอน

GET Request

Headers ใช้สำหรับ caching, virtual hosting, compression แต่สำหรับ basic implementation เราสามารถข้ามได้

สร้าง HTTP parser 簡単 โดยใช้ข้อเท็จจริงว่า request data แยกด้วย new lines (\r\n) ไม่ทำ error/validity checking เพื่อให้ simple

โค้ดที่ผมเขียน:

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

จะคืนค่า hash พร้อม parsed request data ตอนนี้มี request ในรูปแบบใช้งานได้ สร้าง response ให้ client

ขั้นตอน 3: เตรียมและส่ง Response

ต้องเช็คว่า requested resource มีอยู่จริง คือเช็คว่าไฟล์มีอยู่

โค้ด:

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.exist?(path)
    send_ok_response(File.binread(path))
  else
    send_file_not_found
  end
end

มี 2 สิ่งเกิดขึ้น:

  • ถ้า path เป็น / สมมติว่าไฟล์คือ index.html
  • ถ้าเจอไฟล์ ส่งเนื้อหาไฟล์พร้อม OK response

แต่ถ้าไม่เจอ ส่ง response 404 Not Found ทั่วไป

ตาราง HTTP Response Codes ที่พบบ่อย

อ้างอิง

Code คำอธิบาย
200 OK
301 Moved permanently
302 Found
304 Not Modified
400 Bad Request
401 Unauthorized
403 Forbidden
404 Not found
500 Internal Server Error
502 Bad Gateway

Response Class และเมธอด

นี่คือเมธอด "send" ที่ใช้ในตัวอย่างก่อนหน้า:

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

Response สร้างจาก template และ string interpolation

ตอนนี้แค่รวมทุกอย่างใน connection-accepting loop จะได้ functional server

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 files ใต้ SERVER_ROOT และควรโหลดได้จาก browser จะ serve static assets รวมรูปภาพด้วย

當然 web server จริงมีฟีเจอร์เยอะกว่านี้มาก

รายการ บางส่วน ที่ขาดไป ลอง implement เองเป็น exercise (practice is the mother of skill!):

  • Virtual hosting
  • Mime types
  • Data compression
  • Access control
  • Multi-threading
  • Request validation
  • Query string parsing
  • POST body parsing
  • Browser caching (response code 304)
  • Redirects

บทเรียนเรื่อง Security

รับ input จาก user แล้วทำอะไรสักอย่าง อันตรายเสมอ ใน project นี้ user input คือ HTTP request

เราเผลอเปิดช่องโหว่ "path traversal" คนจะอ่านไฟล์ได้ทุกไฟล์ที่ web server user เข้าถึงได้ แม้จะอยู่นอก SERVER_ROOT

บรรทัดที่ทำให้เกิด issue:

File.binread(path)

ลอง exploit เองดู จะต้องทำ "manual" HTTP request เพราะ HTTP clients (รวม curl) จะ pre-process URL และตัดส่วนที่ trigger vulnerability

เครื่องมือหนึ่งคือ netcat

Exploit ที่เป็นไปได้:

$ nc localhost 8080
GET ../../etc/passwd HTTP/1.1

จะคืนเนื้อหา /etc/passwd ถ้าคุณอยู่บน Unix-based system เหตุผลคือ double dot (..) ให้ขึ้นไปหนึ่ง directory "escape" SERVER_ROOT

วิธีแก้หนึ่งคือ "compress" multiple dots เป็นหนึ่ง:

path.gsub!(/\.+/, ".")

คิดเรื่อง security ให้ใส่ "hacker hat" ลองหาวิธีโจมตีโซลูชันของตัวเอง เช่นถ้าแค่ path.gsub!("..", ".") สามารถ bypass ด้วย triple dots (...)

โค้ดเสร็จและทำงานได้

รู้ว่าโค้ดกระจัดกระจายในบทความ ถ้าหา finished, working code...

ลิงก์:

https://gist.github.com/matugm/efe0a1c4fc53310f7ac93dcd1f041f6c#file-web-server-rb

Enjoy!

สรุป

ในบทความนี้ คุณเรียนรู้การ listen connection, HTTP request long อย่างไร และ parse ยังไง สร้าง response ด้วย response code และเนื้อหาไฟล์ (ถ้ามี)

และสุดท้าย เรียนรู้ช่องโหว่ "path traversal" และวิธีป้องกัน

หวังว่าคุณเพลิดเพลินและเรียนรู้สิ่งใหม่! อย่าลืมสมัครรับข่าวสารด้านล่าง จะไม่พลาดบทความใหม่ 🙂