分享
  1. 首页
  2. 文章

Golang动手写一个Http Proxy

杨锡坤 · · 11360 次点击 · · 开始浏览
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

本文主要使用Golang实现一个可用但不够标准,支持basic authentication的http代理服务。

为何说不够标准,在HTTP/1.1 RFC中,有些关于代理实现标准的条目在本文中不考虑。

Http Proxy是如何代理我们的请求


Http 请求的代理如下图,Http Proxy只需要将接收到的请求转发给服务器,然后把服务器的响应,转发给客户端即可。

Https 请求的代理如下图,客户端首先需要发送一个Http CONNECT请求到Http Proxy,Http Proxy建立一条TCP连接到指定的服务器,然后响应200告诉客户端连接建立完成,之后客户端就可以与服务器进行SSL握手和传输加密的Http数据了。

为何需要CONNECT请求? 因为Http Proxy不是真正的服务器,没有www.foo.com的证书,不可能以www.foo.com的身份与客户端完成SSL握手从而建立Https连接。 所以需要通过CONNECT请求告诉Http Proxy,让Http Proxy与服务器先建立好TCP连接,之后客户端就可以将SSL握手消息发送给Http Proxy,再由Http Proxy转发给服务器,完成SSL握手,并开始传输加密的Http数据。

Basic Authentication


为了保护Http Proxy不被未授权的客户端使用,可以要求客户端带上认证信息。这里以Basic Authentication为例。

客户端在与Http Proxy建立连接时,Http请求头中需要带上:

Proxy-Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l

如果服务端验证通过,则正常建立连接,否则响应:

HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic realm="*"

所需要开发的功能模块


  1. 连接处理
  2. 从客户端请求中获取服务器连接信息
  3. 基本认证
  4. 请求转发
连接处理

需要开发一个TCP服务器,因为HTTP服务器没法实现Https请求的代理。

Server的定义:

typeServerstruct{listenernet.Listeneraddrstringcredentialstring}

通过Start方法启动服务,为每个客户端连接创建goroutine为其服务:

// Start a proxy serverfunc(s*Server)Start(){varerrerrors.listener,err=net.Listen("tcp",s.addr)iferr!=nil{servLogger.Fatal(err)}ifs.credential!=""{servLogger.Infof("use %s for auth\n",s.credential)}servLogger.Infof("proxy listen in %s, waiting for connection...\n",s.addr)for{conn,err:=s.listener.Accept()iferr!=nil{servLogger.Error(err)continue}gos.newConn(conn).serve()}}
从客户端请求中获取服务器连接信息

对于http请求头的解析,参考了golang内置的http server。

getTunnelInfo用于获取:

  1. 请求头
  2. 服务器地址
  3. 认证信息
  4. 是否https请求
// getClientInfo parse client request header to get some information:func(c*conn)getTunnelInfo()(rawReqHeaderbytes.Buffer,host,credentialstring,isHttpsbool,errerror){tp:=textproto.NewReader(c.brc)// First line: GET /index.html HTTP/1.0varrequestLinestringifrequestLine,err=tp.ReadLine();err!=nil{return}method,requestURI,_,ok:=parseRequestLine(requestLine)if!ok{err=&BadRequestError{"malformed HTTP request"}return}// https requestifmethod=="CONNECT"{isHttps=truerequestURI="http://"+requestURI}// get remote hosturiInfo,err:=url.ParseRequestURI(requestURI)iferr!=nil{return}// Subsequent lines: Key: value.mimeHeader,err:=tp.ReadMIMEHeader()iferr!=nil{return}credential=mimeHeader.Get("Proxy-Authorization")ifuriInfo.Host==""{host=mimeHeader.Get("Host")}else{ifstrings.Index(uriInfo.Host,":")==-1{host=uriInfo.Host+":80"}else{host=uriInfo.Host}}// rebuild http request headerrawReqHeader.WriteString(requestLine+"\r\n")fork,vs:=rangemimeHeader{for_,v:=rangevs{rawReqHeader.WriteString(fmt.Sprintf("%s: %s\r\n",k,v))}}rawReqHeader.WriteString("\r\n")return}
基本认证
// validateCredentials parse "Basic basic-credentials" and validate itfunc(s*Server)validateCredential(basicCredentialstring)bool{c:=strings.Split(basicCredential," ")iflen(c)==2&&strings.EqualFold(c[0],"Basic")&&c[1]==s.credential{returntrue}returnfalse}
请求转发

serve方法会进行Basic Authentication验证,对于http请求的代理,会把请求头转发给服务器,对于https请求的代理,则会响应200给客户端。

// serve tunnel the client connection to remote hostfunc(c*conn)serve(){deferc.rwc.Close()rawHttpRequestHeader,remote,credential,isHttps,err:=c.getTunnelInfo()iferr!=nil{connLogger.Error(err)return}ifc.auth(credential)==false{connLogger.Error("Auth fail: "+credential)return}connLogger.Info("connecting to "+remote)remoteConn,err:=net.Dial("tcp",remote)iferr!=nil{connLogger.Error(err)return}ifisHttps{// if https, should sent 200 to client_,err=c.rwc.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\n"))iferr!=nil{glog.Errorln(err)return}}else{// if not https, should sent the request header to remote_,err=rawHttpRequestHeader.WriteTo(remoteConn)iferr!=nil{connLogger.Error(err)return}}// build bidirectional-streamsconnLogger.Info("begin tunnel",c.rwc.RemoteAddr(),"<->",remote)c.tunnel(remoteConn)connLogger.Info("stop tunnel",c.rwc.RemoteAddr(),"<->",remote)}

完整代码可查看:https://github.com/yangxikun/gsproxy


有疑问加站长微信联系(非本文作者)

本文来自:yangxikun.github.io

感谢作者:杨锡坤

查看原文:Golang动手写一个Http Proxy

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

关注微信
11360 次点击 ∙ 1 赞
添加一条新回复 (您需要 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传

用户登录

没有账号?注册
(追記) (追記ここまで)

今日阅读排行

    加载中
(追記) (追記ここまで)

一周阅读排行

    加载中

关注我

  • 扫码关注领全套学习资料 关注微信公众号
  • 加入 QQ 群:
    • 192706294(已满)
    • 731990104(已满)
    • 798786647(已满)
    • 729884609(已满)
    • 977810755(已满)
    • 815126783(已满)
    • 812540095(已满)
    • 1006366459(已满)
    • 692541889

  • 关注微信公众号
  • 加入微信群:liuxiaoyan-s,备注入群
  • 也欢迎加入知识星球 Go粉丝们(免费)

给该专栏投稿 写篇新文章

每篇文章有总共有 5 次投稿机会

收入到我管理的专栏 新建专栏