আপনি কি কখনো রুবি দিয়ে নিজের ওয়েব সার্ভার তৈরি করেছেন?
আমাদের ইতিমধ্যে অনেক সার্ভার আছে, যেমন:
- Puma
- Thin
- Unicorn
“하지만 আমি মনে করি এটি একটি দুর্দান্ত শেখার অনুশীলন যদি আপনি জানতে চান একটি সাধারণ ওয়েব সার্ভার কীভাবে কাজ করে।
এই আর্টিকেলে, আপনি শিখবেন কীভাবে এটি করতে হয়।
ধাপে ধাপে!
ধাপ ১: কানেকশন শোনা
কোথায় থেকে শুরু করব?
প্রথমে যা हमें দরকার তা হল TCP পোর্ট ৮০-এ নতুন কানেকশন শোনা।
আমি ইতিমধ্যে রুবিতে নেটওয়ার্ক প্রোগ্রামিং সম্পর্কে একটি পোস্ট লিখেছি, তাই এখানে সেটি কীভাবে কাজ করে সেটি ব্যাখ্যা করব না।
আমি শুধু কোডটি দিচ্ছি:
require 'socket'
server = TCPServer.new('localhost', 80)
loop {
client = server.accept
request = client.readpartial(2048)
puts request
}
এই কোড রান করালে আপনার একটি সার্ভার হবে যা পোর্ট ৮০-এ কানেকশন এক্সেপ্ট করবে। এটি এখনো বেশি কিছু করছে না, কিন্তু এটি আপনাকে একটি ইনকামিং রিকোয়েস্ট কীভাবে দেখাবে সেটি দেখতে দেবে।
নোট: লিনাক্স/ম্যাক সিস্টেমে পোর্ট ৮০ ব্যবহার করতে রুট প্রিভিলেজ লাগবে। বিকল্প হিসেবে, আপনি ১০২৪-এর উপরে যেকোনো পোর্ট ব্যবহার করতে পারেন। আমি ৮০৮০ পছন্দ করি 🙂
রিকোয়েস্ট জেনারেট করার একটি সহজ উপায় হল আপনার ব্রাউজার বা curl এর মতো কিছু ব্যবহার করা।
যখন আপনি তা করবেন আপনি আপনার সার্ভারে এটি প্রিন্ট হওয়া দেখবেন:
GET / HTTP/1.1
Host: localhost
User-Agent: curl/7.49.1
Accept: */*
এটি একটি HTTP রিকোয়েস্ট। HTTP হল একটি প্লেইন-টেক্সট প্রোটোকল যা ওয়েব ব্রাউজার এবং ওয়েব সার্ভারের মধ্যে যোগাযোগের জন্য ব্যবহৃত হয়।
আফিসিয়াল প্রোটোকল স্পেসিফিকেশন এখানে পাওয়া যাবে: https://tools.ietf.org/html/rfc7230।
ধাপ ২: রিকোয়েস্ট পার্স করা
এখন আমাদের রিকোয়েস্টকে ছোট ছোট কম্পোনেন্টে ভাগ করতে হবে যাতে আমাদের সার্ভার সেটি বুঝতে পারে।
এটা করতে আমরা নিজেদের পার্সার বানাতে পারি বা যেটি ইতিমধ্যে আছে সেটি ব্যবহার করতে পারি। আমরা নিজেদের বানাবো তাই আমাদের বুঝতে হবে রিকোয়েস্টের বিভিন্ন অংশের অর্থ কী।
এই ছবিটি সাহায্য করবে:
GET রিকোয়েস্ট
হেডারগুলো ব্রাউজার ক্যাশিং, ভার্চুয়াল হোস্টিং এবং ডেটা কমপ্রেশন এর জন্য ব্যবহৃত হয়, কিন্তু একটি বেসিক ইমপ্লিমেন্টেশনের জন্য আমরা সেগুলো ইগনোর করতে পারি এবং এখনও একটি ফাংশনাল সার্ভার পেতে পারি।
একটি সিম্পল 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
end
def normalize(header)
header.gsub(":", "").downcase.to_sym
end
এটি পার্স করা রিকোয়েস্ট ডেটার সাথে একটি হ্যাশ রিটার্ন করবে। এখন যেহেতু আমাদের রিকোয়েস্টটি ব্যবহারযোগ্য ফরম্যাটে আছে, আমরা ক্লায়েন্টের জন্য রেসপন্স বানাতে পারি।
ধাপ ৩: রেসপন্স প্রস্তুত ও পাঠানো
রেসপন্স বানাতে হলে আমাদের দেখতে হবে অনুরোধ করা রিসোর্সটি উপলব্ধ কিনা। অন্য কথায়, আমাদের চেক করতে হবে ফাইলটি এক্সিস্ট করে কিনা।
এখানে সেই কাজের জন্য আমার লেখা কোড:
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। - দ্বিতীয়, যদি অনুরোধ করা ফাইলটি পাওয়া যায়, তবে আমরা ফাইলের কন্টেন্টস একটি OK রেসপন্সের সাথে পাঠাব।
কিন্তু যদি ফাইলটি না পাওয়া যায় তবে আমরা টিপিক্যাল 404 Not Found রেসপন্স পাঠাব।
সবচেয়ে সাধারণ HTTP রেসপন্স কোডের টেবিল
রেফারেন্সের জন্য।
| কোড | বিবরণ |
|---|---|
| 200 | OK |
| 301 | Permanently Moved |
| 302 | Found |
| 304 | Not Modified |
| 400 | Bad Request |
| 401 | Unauthorized |
| 403 | Forbidden |
| 404 | Not Found |
| 500 | Internal Server Error |
| 502 | Bad Gateway |
Response ক্লাস ও মেথডসমূহ
এখানে পূর্বের উদাহরণে ব্যবহৃত "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
রেসপন্সটি একটি টেমপ্লেট এবং কিছু স্ট্রিং ইন্টারপোলেশন থেকে তৈরি হয়।
এই পয়েন্টে আমাদের কানেকশন-এক্সেপ্টিং 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
}
SERVER_ROOT ডিরেক্টরির নিচে কিছু HTML ফাইল যোগ করে দেখুন, আপনি আপনার ব্রাউজার থেকে সেগুলো লোড করতে পারবেন। এটি ইমেজ সহ যেকোনো অন্যান্য স্ট্যাটিক অ্যাসেটও সার্ভ করবে।
নিশ্চয়ই একটি রিয়েল ওয়েব-সার্ভারের অনেক অধিক ফিচার আছে যা আমরা এখানে কভার করিনি।
এখানে কিছু মিসিং ফিচারের তালিকা, যাতে আপনি সেগুলো নিজে ইমপ্লিমেন্ট করতে পারেন একটি এক্সারসাইজ হিসেবে (অনুশীলনই দক্ষতার মা!):
- ভার্চুয়াল হোস্টিং
- Mime types
- ডেটা কমপ্রেশন
- অ্যাক্সেস কন্ট্রোল
- মাল্টি-থ্রেডিং
- রিকোয়েস্ট ভ্যালিডেশন
- কোয়েরি স্ট্রিং পার্সিং
- POST বডি পার্সিং
- ব্রাউজার ক্যাশিং (রেসপন্স কোড 304)
- রিডাইরেক্টস
সিকিউরিটির উপর একটি পাঠ
ইউজার থেকে ইনপুট নেয়া এবং সেটির সাথে কিছু করা সবসময় খতরনাক। আমাদের ছোট ওয়েব সার্ভার প্রজেক্টে, ইউজার ইনপুট হল HTTP রিকোয়েস্ট।
আমরা একটি ছোট ভালনারেবিলিটি চালু করেছি যাকে "পাথ ট্রাভারসাল" বলা হয়। লোকেরা সেই সকল ফাইল পড়তে পারবে যেগুলো আমাদের ওয়েব সার্ভার ইউজারের কাছে এক্সেসIBLE, এমনকি যদি সেগুলো আমাদের SERVER_ROOT ডিরেক্টরির বাইরে থাকে।
এই লাইনটি এই সমস্যার জন্য দায়ী:
File.binread(path)
আপনি নিজে এই ইস্যু এক্সপ্লয়েট করার চেষ্টা করতে পারেন এটি একশনে দেখতে। আপনাকে একটি "ম্যানুয়াল" HTTP রিকোয়েস্ট করতে হবে, কারণ অধিকাংশ HTTP ক্লায়েন্ট ( curl সহ) আপনার URL প্রি-প্রসেস করবে এবং ভালনারেবিলিটি ট্রিগার করার অংশটি রিমুভ করে দেবে।
একটি টুল যা আপনি ব্যবহার করতে পারেন সেটি হল netcat।
এখানে একটি সম্ভাব্য এক্সপ্লয়েট:
$ nc localhost 8080
GET ../../etc/passwd HTTP/1.1
যদি আপনি ইউনিক্স-ভিত্তিক সিস্টেমে থাকেন তবে এটি /etc/passwd ফাইলের কন্টেন্টস রিটার্ন করবে। এর কাজ করার কারণ হল ডাবল ডট (..) আপনাকে এক ডিরেক্টরি উপরে যেতে দেয়, তাই আপনি SERVER_ROOT ডিরেক্টরির বাইরে "এস্কেপ" করছেন।
একটি সম্ভাব্য সমাধান হল একাধিক ডটকে একটি করে "কমপ্রেস" করা:
path.gsub!(/\.+/, ".")
সিকিউরিটি সম্পর্কে চিন্তা করার সময় সবসময় আপনার "হ্যাকার হ্যাট" पहনুন এবং আপনার সমাধান ভাঙতে উপায় খুঁজুন। উদাহরণস্বরূপ, যদি আপনি শুধু path.gsub!("..", ".") করেন, তবে ট্রিপল ডট (...) ব্যবহার করে সেটি বাইপাস করা সম্ভব।
সম্পূর্ণ ওয়ার্কিং কোড
আমি জানি এই পোস্টে কোড ইনকোথাও ছড়িয়ে পড়েছে, তাই যদি আপনি ফাইনাল, ওয়ার্কিং কোড খুঁজছেন…
এখানে লিংকটি:
https://gist.github.com/matugm/efe0a1c4fc53310f7ac93dcd1f041f6c#file-web-server-rb
এনজয় করুন!
সারসংক্ষেপ
এই পোস্টে, আপনি শিখেছেন কীভাবে নতুন কানেকশন শোনা হয়, একটি HTTP রিকোয়েস্ট কেমন দেখে এবং কীভাবে এটি পার্স করতে হয়। আপনি এছাড়া শিখেছেন কীভাবে একটি রেসপন্স কোড এবং প্রয়োজনীয় ফাইলের কন্টেন্টস (যদি উপলব্ধ থাকে) ব্যবহার করে রেসপন্স বানাতে হয়।
এবং অবশেষে আপনি "পাথ ট্রাভারসাল" ভালনারেবিলিটি এবং কীভাবে এড়াতে হয় সেটি শিখেছেন।
আশা করি আপনি এই পোস্টটি উপভোগ করেছেন এবং কিছু নতুন শিখেছেন! নিচের ফর্ম থেকে আমার নিউজলেটার সাবস্ক্রাইব করা ভুলবেন না, যাতে আপনি একক পোস্টও মিস না করেন 🙂