Bạn đã bao giờ xây dựng máy chủ web của riêng mình bằng Ruby chưa?
Chúng tôi đã có nhiều máy chủ, như:
- Puma
- Mỏng
- Kỳ lân
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 cách một máy chủ web đơn giản hoạt động.
Trong bài viết này, bạn sẽ học cách thực hiện việc này.
Từng bước!
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 các 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 bằng Ruby, vì vậy tôi sẽ không giải thích cách hoạt động của nó ở đây.
Tôi chỉ định cung cấp 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 máy chủ chấp nhận các kết nối trên cổng 80. Nó chưa có tác dụng gì nhiều nhưng nó sẽ cho phép bạn xem một yêu cầu đến trông như thế nào.
Lưu ý :Để sử dụng cổng 80 trong hệ thống Linux / Mac, bạn sẽ cần có đặc quyền root. Để thay thế, bạn có thể sử dụng một cổng khác trên 1024. Tôi thích 8080 🙂
Một cách dễ dàng để tạo yêu cầu là chỉ sử dụng trình duyệt của bạn hoặc một cái gì đó như curl
.
Khi làm điều đó, bạn sẽ thấy thông báo này được in trong máy chủ của mình:
GET / HTTP/1.1 Host: localhost User-Agent: curl/7.49.1 Accept: */*
Đây là một yêu cầu HTTP. HTTP là một giao thức văn bản thuần túy được sử dụng để giao tiếp giữa các trình duyệt web và máy chủ web.
Đặc tả giao thức chính thức có thể được tìm thấy tại đây:https://tools.ietf.org/html/rfc7230.
Bước 2:Phân tích cú pháp Yêu cầu
Bây giờ chúng ta cần chia nhỏ yêu cầu thành các thành phần nhỏ hơn mà máy chủ của chúng ta có thể hiểu được.
Để làm điều đó, chúng tôi có thể xây dựng trình phân tích cú pháp của riêng mình hoặc sử dụng trình phân tích cú pháp đã tồn tại. Chúng tôi sẽ tự xây dựng nên chúng tôi cần hiểu ý nghĩa của các phần khác nhau của yêu cầu.
Hình ảnh này sẽ hữu ích :
NHẬN Yêu cầu
Các tiêu đề được sử dụng cho những thứ như bộ nhớ đệm của trình duyệt, lưu trữ ảo và nén dữ liệu, nhưng để triển khai cơ bản, chúng tôi có thể bỏ qua chúng và vẫn có một máy chủ chức năng.
Để xây dựng một trình phân tích cú pháp HTTP đơn giản, chúng ta có thể tận dụng thực tế là dữ liệu yêu cầu được phân tách qua các dòng mới (\r\n
). Chúng tôi sẽ không thực hiện bất kỳ lỗi hoặc kiểm tra tính hợp lệ nào để 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
Thao tác này sẽ trả về một hàm băm với dữ liệu yêu cầu đã được phân tích cú pháp. Bây giờ chúng tôi có yêu cầu của mình ở định dạng có thể sử dụng được, chúng tôi có thể xây dựng phản hồi của chúng tôi cho khách hàng.
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ó khả dụng hay không. Nói cách khác, chúng tôi cần kiểm tra xem tệp có tồn tại hay 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 đang xảy ra ở đây :
- Đầu tiên, nếu đường dẫn được đặt thành
/
chúng tôi giả định rằng tệp chúng tôi muốn làindex.html
. - Thứ hai, nếu tệp được yêu cầu được tìm thấy, chúng tôi sẽ gửi nội dung tệp với phản hồi OK.
Nhưng nếu tệp không được tìm thấy thì chúng tôi sẽ gửi 404 Not Found
điển hình phản hồi.
Bảng mã phản hồi HTTP phổ biến nhất
Để tham khảo.
Mã | Mô tả |
---|---|
200 | Được |
301 | Đã chuyển vĩnh viễn |
302 | Đã tìm thấy |
304 | Không được sửa đổi |
400 | Yêu cầu không hợp lệ |
401 | Trái phép |
403 | Bị cấm |
404 | Không tìm thấy |
500 | Lỗi Máy chủ Nội bộ |
502 | Cổng sai |
Lớp và phương pháp phản hồi
Dưới đây là các phương thức "gửi" được sử dụng trong ví dụ cuối cùng:
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à Response
lớp:
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 tạo từ một mẫu và một số nội suy chuỗi.
Tại thời điểm này, chúng ta chỉ cần kết hợp mọi thứ lại với nhau trong vòng lặp loop
và sau đó chúng ta sẽ có một máy chủ 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 }
Thử thêm một số tệp HTML trong SERVER_ROOT
và bạn có thể tải chúng từ trình duyệt của mình. Điều này cũng sẽ phân phát bất kỳ nội dung tĩnh nào khác, bao gồm cả hình ảnh.
Tất nhiên, một máy chủ web thực có nhiều tính năng khác mà chúng tôi không đề cập ở đây.
Đây là danh sách một số các tính năng còn thiếu, vì vậy bạn có thể tự triển khai chúng như một bài tập (thực hành là mẹ của kỹ năng!):
- Lưu trữ ảo
- Các loại kịch câm
- Nén dữ liệu
- Kiểm soát truy cập
- Đa luồng
- Yêu cầu xác thực
- Phân tích cú pháp chuỗi truy vấn
- ĐĂNG phân tích cú pháp nội dung
- Bộ nhớ đệm của trình duyệt (mã phản hồi 304)
- Chuyển hướng
Bài học về bảo mật
Lấy thông tin đầu vào từ người dùng và làm điều gì đó với nó luôn nguy hiểm. Trong dự án máy chủ web nhỏ của chúng tôi, đầu vào của người dùng là yêu cầu HTTP.
Chúng tôi đã giới thiệu một lỗ hổng nhỏ được gọi là "đường dẫn truyền". Mọi người sẽ có thể đọc bất kỳ tệp nào mà người dùng máy chủ web của chúng tôi có quyền truy cập, ngay cả khi chúng nằm ngoài SERVER_ROOT
của chúng tôi thư mục.
Đây là dòng chịu trách nhiệm cho vấn đề này:
File.binread(path)
Bạn có thể thử tự mình khai thác vấn đề này để xem nó hoạt động như thế nào. Bạn sẽ cần thực hiện một yêu cầu HTTP “thủ công” vì hầu hết các ứng dụng HTTP (bao gồm curl
) sẽ xử lý trước URL của bạn và xóa phần gây ra lỗ hổng bảo mật.
Một công cụ bạn có thể sử dụng được gọi là netcat.
Đây là một cách có thể khai thác:
$ nc localhost 8080 GET ../../etc/passwd HTTP/1.1
Thao tác này sẽ trả về nội dung của /etc/passwd
nếu bạn đang sử dụng hệ thống dựa trên Unix. Lý do điều này hoạt động là do dấu chấm kép (..
) cho phép bạn đi lên một thư mục, vì vậy bạn đang "thoát" SERVER_ROOT
thư mục.
Một giải pháp khả thi là "nén" nhiều dấu chấm thành một:
path.gsub!(/\.+/, ".")
Khi nghĩ về bảo mật, hãy luôn đặt “cái mũ tin tặc” của bạn và cố gắng tìm cách phá vỡ giải pháp của bạn. Ví dụ:nếu bạn vừa làm path.gsub!("..", ".")
, bạn có thể bỏ qua điều đó bằng cách sử dụng dấu ba chấm (...
).
Mã hoàn thành &làm việc
Tôi biết mã ở khắp nơi trong bài đăng này, vì vậy nếu bạn đang tìm mã hoàn chỉnh, đang hoạt động…
Đây là liên kết :
https://gist.github.com/matugm/efe0a1c4fc53310f7ac93dcd1f041f6c#file-web-server-rb
Hãy tận hưởng!
Tóm tắt
Trong bài đăng này, bạn đã học cách lắng nghe các kết nối mới, yêu cầu HTTP trông như thế nào và cách phân tích cú pháp. Bạn cũng đã học cách tạo phản hồi bằng cách sử dụng mã phản hồi và nội dung của tệp bắt buộc (nếu có).
Và cuối cùng, bạn đã tìm hiểu về lỗ hổng "truyền qua đường dẫn" và cách tránh nó.
Tôi hy vọng bạn thích bài đăng này và học được điều gì đó mới! Đừng quên đăng ký nhận bản tin của tôi ở biểu mẫu bên dưới, để bạn sẽ không bỏ lỡ một bài đăng nào 🙂