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단계:요청 구문 분석
이제 요청을 서버가 이해할 수 있는 더 작은 구성 요소로 분해해야 합니다.
그렇게 하기 위해 우리는 우리 자신의 파서를 만들거나 이미 존재하는 것을 사용할 수 있습니다. 요청의 다른 부분이 의미하는 바를 이해해야 하므로 자체적으로 빌드할 것입니다.
이 이미지는 도움이 됩니다 :
요청 받기
헤더는 브라우저 캐싱, 가상 호스팅 및 데이터 압축과 같은 작업에 사용되지만 기본 구현의 경우 헤더를 무시하고 여전히 작동하는 서버를 보유할 수 있습니다.
간단한 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 요청이 어떻게 보이는지 및 구문 분석하는 방법을 배웠습니다. 또한 응답 코드와 필수 파일의 내용(사용 가능한 경우)을 사용하여 응답을 작성하는 방법도 배웠습니다.
그리고 마지막으로 "경로 순회" 취약점과 이를 피하는 방법에 대해 배웠습니다.
이 게시물을 즐기고 새로운 것을 배웠기를 바랍니다! 아래 양식에서 내 뉴스레터를 구독하는 것을 잊지 마세요. 그러면 게시물 하나를 놓치지 않을 것입니다 🙂