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 요청이 어떻게 보이는지 및 구문 분석하는 방법을 배웠습니다. 또한 응답 코드와 필수 파일의 내용(사용 가능한 경우)을 사용하여 응답을 작성하는 방법도 배웠습니다.
그리고 마지막으로 "경로 순회" 취약점과 이를 피하는 방법에 대해 배웠습니다.
이 게시물을 즐기고 새로운 것을 배웠기를 바랍니다! 아래 양식에서 내 뉴스레터를 구독하는 것을 잊지 마세요. 그러면 게시물 하나를 놓치지 않을 것입니다 🙂