본문 바로가기
golang

[golang] golang reverse proxy 예제

by devjh 2023. 4. 6.
반응형

일반적으로 proxy기능이 필요한 경우 nginx, haproxy 등 proxy용 솔루션을 통해 구축하지만 내부적으로 추가 동작이 필요한 경우 직접 proxy 서버를 개발해야 됩니다.

 

이번 게시글에서는 golang을 사용해 reverse proxy를 구축하는 방법에 대해 정리합니다.

 

저는 http request를 처리하기위해 echo framework를 사용하는것을 선호하지만, 이번 게시글에서는 따로 서드파티 라이브러리의 기능이필요하지 않으므로 standard library인 mux를 이용한 프록시 서버를 구축합니다.

1. 소스코드

소스코드는 아래 github repo에서 확인할 수 있습니다.

 

GitHub - jaeho310/go-std-proxy

Contribute to jaeho310/go-std-proxy development by creating an account on GitHub.

github.com

 

2. 프로젝트 구조

.
├── go.mod
├── main.go
├── handler
 │   ├── default_handler.go
 │   └── proxy_handler.go
└── server
    └── server.go

 

3. main.go

package main

import (
   "fmt"
   "go-std-proxy/server"
)

func main() {
   fmt.Println("proxy-server start")
   server.InitProxyServer()
}

 

프로그램 진입점에서는

servier.InitProxyServer()를 호출합니다.

4. server.go

package server

import (
   "go-std-proxy/handler"
   "net/http"
)

func InitProxyServer() {
   mux := http.NewServeMux()
   handler.InitDefaultHandler(mux)
   handler.InitProxyHandler(mux)
   err := http.ListenAndServe(":8081", mux)
   if err != nil {
      panic(err)
   }
}

standard library 인 net/http의 mux instance를 생성해준후 default handler와 proxy handler에게 넘겨주고

8081번 포트로 http request가 온 경우 처리할수 있게 서버를 시작합니다.

 

5. default_handler.go

package handler

import (
   "fmt"
   "net/http"
)

func InitDefaultHandler(mux *http.ServeMux) {
   mux.HandleFunc("/health", health)
}

func health(w http.ResponseWriter, r *http.Request) {
   fmt.Fprintf(w, "hello~")
}

default handler에서는 health 체크를 넣어줍니다.

6. proxy_handler.go

package handler

import (
   "context"
   "crypto/tls"
   "fmt"
   "log"
   "net"
   "net/http"
   "net/http/httputil"
   "net/url"
   "strings"
)

func InitProxyHandler(mux *http.ServeMux) {
   mux.HandleFunc("/", handleProxy)
}

func handleProxy(w http.ResponseWriter, r *http.Request) {
   host := getHost(r)
   path := r.URL.Path
   fmt.Printf("host: [%s], path: [%s]\n", host, path)
   var target *url.URL
   if path == "/" {
      target, _ = url.Parse("https://naver.com")
   } else {
      u := fmt.Sprintf("https://search.naver.com/search.naver?query=%s", path[1:])
      target, _ = url.Parse(u)
   }
   reverseProxy := httputil.NewSingleHostReverseProxy(target)
   // 프록시 서버 자체는 http 서버입니다만
   // https 요청을 처리해야 하는 경우에는 아래의 내용을 추가합니다.
   // mixed contents 등의 에러를 피해갈 수 있습니다.
   reverseProxy.Transport = &http.Transport{
      DialTLSContext:  dialTLS,
      TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
   }
   reverseProxy.ServeHTTP(w, r)
}

func getHost(r *http.Request) string {
   i := strings.Index(r.Host, ":")
   if i > 0 {
      return r.Host[:i]
   }
   return r.Host
}

func dialTLS(ctx context.Context, network, addr string) (net.Conn, error) {
   conn, err := net.Dial(network, addr)
   if err != nil {
      return nil, err
   }

   // host의 변경이 필요한경우 여기서 변경을 줄수 있습니다.
   host, _, err := net.SplitHostPort(addr)
   if err != nil {
      return nil, err
   }
   cfg := &tls.Config{ServerName: host}

   tlsConn := tls.Client(conn, cfg)
   if err := tlsConn.Handshake(); err != nil {
      conn.Close()
      return nil, err
   }

   cs := tlsConn.ConnectionState()
   cert := cs.PeerCertificates[0]

   cert.VerifyHostname(host)
   log.Println(cert.Subject)

   return tlsConn, nil
}

 

health 요청을 제외한 모든 요청은 '/' 를 받기로 한 handler가 처리합니다.

 

http://localhost:8081 로 요청이 오면 naver페이지로
http://localhost:8081/path 로 요청이 오면 path에 맞게 네이버 검색을 해주는 프록시 서버입니다.

mixed contents 에러가 발생하는경우 주석으로 설명한 내용을 추가합니다.

 

브라우저에서 localhost로 직접 붙는경우는 host가 localhost로 동일하지만,
dns, lb등에서 해당 서버로 요청을 보내주는 경우 host를 이용하여 프록시의 추가 동작을 진핼할 수도 있다는 장점이 있어, host를 로그로 남깁니다.

반응형

댓글