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에서 연결을 수락하는 서버를 갖게 됩니다. 아직 많은 작업을 수행하지 않지만 들어오는 요청이 어떻게 보이는지 볼 수 있습니다.

<블록 인용>

참고 :Linux/Mac 시스템에서 포트 80을 사용하려면 루트 권한이 필요합니다. 대안으로 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단계:요청 구문 분석

이제 요청을 서버가 이해할 수 있는 더 작은 구성 요소로 분해해야 합니다.

그렇게 하기 위해 우리는 우리 자신의 파서를 만들거나 이미 존재하는 것을 사용할 수 있습니다. 요청의 다른 부분이 의미하는 바를 이해해야 하므로 자체적으로 빌드할 것입니다.

이 이미지는 도움이 됩니다 :

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이라고 가정합니다. .
  • 둘째, 요청된 파일이 발견되면 OK 응답과 함께 파일 내용을 보내드립니다.

그러나 파일을 찾을 수 없으면 일반적인 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
}

SERVER_ROOT 아래에 HTML 파일을 추가해 보세요. 디렉토리에 있고 브라우저에서 로드할 수 있어야 합니다. 이것은 이미지를 포함한 다른 모든 정적 자산도 제공합니다.

물론 실제 웹 서버에는 여기에서 다루지 않은 더 많은 기능이 있습니다.

다음은 일부 목록입니다. 누락된 기능이 있으므로 연습으로 직접 구현할 수 있습니다(연습은 기술의 어머니입니다!):

  • 가상 호스팅
  • MIME 유형
  • 데이터 압축
  • 액세스 제어
  • 멀티 스레딩
  • 인증 요청
  • 쿼리 문자열 파싱
  • POST 본문 구문 분석
  • 브라우저 캐싱(응답 코드 304)
  • 리디렉션

보안에 대한 강의

사용자로부터 입력을 받아 무언가를 하는 것은 항상 위험합니다. 우리의 작은 웹 서버 프로젝트에서 사용자 입력은 HTTP 요청입니다.

우리는 "경로 순회"로 알려진 약간의 취약점을 도입했습니다. 사람들은 SERVER_ROOT 외부에 있더라도 웹 서버 사용자가 액세스할 수 있는 모든 파일을 읽을 수 있습니다. 디렉토리.

다음은 이 문제에 대한 책임입니다.

File.binread(path)

이 문제를 직접 악용하여 실제로 작동하는지 확인할 수 있습니다. 대부분의 HTTP 클라이언트(curl 포함) 때문에 "수동" HTTP 요청을 만들어야 합니다. )은 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 요청이 어떻게 보이는지 및 구문 분석하는 방법을 배웠습니다. 또한 응답 코드와 필수 파일의 내용(사용 가능한 경우)을 사용하여 응답을 작성하는 방법도 배웠습니다.

그리고 마지막으로 "경로 순회" 취약점과 이를 피하는 방법에 대해 배웠습니다.

이 게시물을 즐기고 새로운 것을 배웠기를 바랍니다! 아래 양식에서 내 뉴스레터를 구독하는 것을 잊지 마세요. 그러면 게시물 하나를 놓치지 않을 것입니다 🙂