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

สร้างเว็บเซิร์ฟเวอร์ของคุณเองด้วย Ruby

คุณเคยสร้างเว็บเซิร์ฟเวอร์ของคุณเองด้วย 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 ของเราเองหรือใช้อันที่มีอยู่แล้ว เรากำลังจะสร้างของเราเอง ดังนั้นเราต้องเข้าใจว่าส่วนต่างๆ ของคำขอหมายถึงอะไร

ภาพนี้น่าจะช่วยได้ :

สร้างเว็บเซิร์ฟเวอร์ของคุณเองด้วย Ruby

รับคำขอ

ส่วนหัวใช้สำหรับสิ่งต่างๆ เช่น การแคชเบราว์เซอร์ การโฮสต์เสมือน และการบีบอัดข้อมูล แต่สำหรับการใช้งานพื้นฐาน เราสามารถเพิกเฉยได้และยังคงมีเซิร์ฟเวอร์ที่ใช้งานได้

ในการสร้างตัวแยกวิเคราะห์ 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 หน้าตาเป็นอย่างไร และวิธีแยกวิเคราะห์ คุณยังได้เรียนรู้วิธีสร้างการตอบกลับโดยใช้รหัสตอบกลับและเนื้อหาของไฟล์ที่จำเป็น (ถ้ามี)

และในที่สุด คุณได้เรียนรู้เกี่ยวกับช่องโหว่ "การข้ามเส้นทาง" และวิธีหลีกเลี่ยง

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