Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Ruby

Xây dựng Custom Web Server bằng Ruby từ con số 0: Hướng dẫn từng bước

Bạn có bao giờ tự xây dựng web server của mình với Ruby?

Chúng ta đã có nhiều server, như:

  • Puma
  • Thin
  • Unicorn

Nhưng tôi nghĩ đây là một bài tập học tập tuyệt vời nếu bạn muốn biết một web server đơn giản hoạt động như thế nào.

Trong bài viết này, bạn sẽ học cách làm điều này.

Từng bước một!

Bước 1: Lắng nghe kết nối

Chúng ta bắt đầu từ đâu?

Điều đầu tiên chúng ta cần là lắng nghe kết nối mới trên cổng TCP 80.

Tôi đã viết một bài về lập trình mạng trong Ruby, nên tôi không giải thích cách đó hoạt động ở đây.

Tôi chỉ đưa cho bạn mã:

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

Khi bạn chạy mã này, bạn sẽ có một server chấp nhận kết nối trên cổng 80. Nó chưa làm nhiều, nhưng sẽ cho bạn thấy yêu cầu đến trông như thế nào.

Lưu ý: Để dùng cổng 80 trên Linux/Mac, bạn cần quyền root. Thay thế, bạn có thể dùng cổng khác trên 1024. Tôi thích 8080 🙂

Cách dễ dàng tạo yêu cầu là dùng trình duyệt hoặc cái gì đó như curl.

Khi bạn làm điều đó, bạn sẽ thấy điều này in ra trong server:

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

Đây là một yêu cầu HTTP. HTTP là giao thức văn bản thuần túy dùng cho giao tiếp giữa trình duyệt web và web server.

Đặc tả giao thức chính thức có thể tìm thấy tại: https://tools.ietf.org/html/rfc7230.

Bước 2: Phân tích yêu cầu (Parsing Request)

Giờ chúng ta cần phân rã yêu cầu thành các thành phần nhỏ hơn mà server có thể hiểu.

Để làm điều đó, chúng ta có thể xây dựng parser riêng hoặc dùng cái đã có. Chúng ta sẽ tự xây để hiểu các phần khác nhau của yêu cầu có nghĩa gì.

Hình ảnh này sẽ giúp:

Xây dựng Custom Web Server bằng Ruby từ con số 0: Hướng dẫn từng bước

GET Request

Các header được dùng cho cache trình duyệt, virtual hosting và nén dữ liệu, nhưng cho triển khai cơ bản, chúng ta có thể bỏ qua chúng và vẫn có server chức năng.

Để xây dựng HTTP parser đơn giản, chúng ta có thể tận hưởng việc dữ liệu yêu cầu được tách biệt qua dòng mới (\r\n). Chúng ta không làm kiểm tra lỗi hoặc hợp lệ để giữ mọi thứ đơn giản.

Đây là mã tôi nghĩ ra:

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

Điều này sẽ trả về hash với dữ liệu yêu cầu đã phân tích. Giờ chúng ta có yêu cầu ở định dạng dùng được, chúng ta có thể xây dựng phản hồi cho client.

Bước 3: Chuẩn bị & Gửi phản hồi

Để xây dựng phản hồi, chúng ta cần xem tài nguyên được yêu cầu có sẵn không. Nói cách khác, chúng ta cần kiểm tra xem tệp có tồn tại không.

Đây là mã tôi viết để làm điều đó:

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

Có hai điều xảy ra ở đây:

  • Thứ nhất, nếu path được thiết lập là /, chúng ta giả định tệp chúng ta muốn là index.html.
  • Thứ hai, nếu tệp được yêu cầu tìm thấy, chúng ta sẽ gửi nội dung tệp với phản hồi OK.

Nhưng nếu tệp không tìm thấy, chúng ta sẽ gửi phản hồi điển hình 404 Not Found.

Bảng các mã phản hồi HTTP phổ biến nhất

Để tham khảo.

Mã Mô tả
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

Lớp Response & Phương thức

Đây là các phương thức "send" được dùng trong ví dụ trước:

def send_ok_response(data)
  Response.new(code: 200, data: data)
end
def send_file_not_found
  Response.new(code: 404)
end

Và đây là lớp 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

Phản hồi được xây dựng từ một template và một chút string interpolation.

Lúc này, chúng ta chỉ cần gắn mọi thứ lại với nhau trong vòng lặp loop chấp nhận kết nối và chúng ta sẽ có một server chức năng.

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
}

Hãy thử thêm một số tệp HTML dưới thư mục SERVER_ROOT và bạn nên có thể tải chúng từ trình duyệt. Điều này cũng sẽ phục vụ bất kỳ tài sản tĩnh nào khác, bao gồm hình ảnh.

Tất nhiên, một web server thực tế có nhiều tính năng hơn mà chúng ta không đề cập ở đây.

Dưới đây là danh sách một số tính năng thiếu, để bạn tự triển khai như bài tập (thực hành là mẹ của kỹ năng!):

  • Virtual hosting
  • Mime types
  • Data compression
  • Access control
  • Multi-threading
  • Request validation
  • Query string parsing
  • POST body parsing
  • Browser caching (mã phản hồi 304)
  • Redirects

Bài học về Bảo mật

Lấy đầu vào từ người dùng và làm gì đó với nó luôn nguy hiểm. Trong dự án web server nhỏ của chúng ta, đầu vào người dùng là yêu cầu HTTP.

Chúng ta đã giới thiệu một lỗ hổng nhỏ gọi là "path traversal" (duyệt đường dẫn). Mọi người sẽ có thể đọc bất kỳ tệp nào mà user web server có quyền truy cập, ngay cả khi chúng nằm ngoài thư mục SERVER_ROOT.

Đây là dòng mã chịu trách nhiệm cho vấn đề này:

File.binread(path)

Bạn có thể thử khai thác vấn đề này để thấy nó hoạt động. Bạn sẽ cần tạo một yêu cầu HTTP "thủ công", vì hầu hết HTTP client (kể cả curl) sẽ tiền xử lý URL và loại bỏ phần kích hoạt lỗ hổng.

Một công cụ bạn có thể dùng là netcat.

Đây là một khai thác có thể:

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

Điều này sẽ trả về nội dung tệp /etc/passwd nếu bạn đang trên hệ thống Unix-based. Lý do nó hoạt động là vì dấu chấm kép (..) cho phép bạn đi lên một thư mục, nên bạn đang "trốn thoát" khỏi thư mục SERVER_ROOT.

Một giải pháp có thể là "nén" nhiều chấm thành một:

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

Khi nghĩ về bảo mật, luôn đội "mũ hacker" và thử tìm cách phá vỡ giải pháp của bạn. Ví dụ, nếu bạn chỉ làm path.gsub!("..", "."), bạn có thể bypass bằng cách dùng chấm ba (...).

Mã hoàn chỉnh & Hoạt động

Tôi biết mã nằm rải rác khắp bài viết, nên nếu bạn tìm mã hoàn chỉnh, hoạt động...

Đây là link:

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

Thưởng thức!

Tóm tắt

Trong bài viết này, bạn đã học cách lắng nghe kết nối mới, yêu cầu HTTP trông như thế nào và cách phân tích nó. Bạn cũng học cách xây dựng phản hồi dùng mã phản hồi và nội dung tệp yêu cầu (nếu có).

Và cuối cùng bạn đã học về lỗ hổng "path traversal" và cách tránh nó.

Tôi hy vọng bạn thích bài viết này và học được điều gì đó mới! Đừng quên đăng ký newsletter của tôi trên form bên dưới, để bạn không bỏ lỡ bất kỳ bài viết nào 🙂