Skip to content

Latest commit

 

History

History
691 lines (499 loc) · 31.5 KB

09.md

File metadata and controls

691 lines (499 loc) · 31.5 KB

九、Web 应用

Go 在标准库中有一个强大的 HTTP 包。net/http包记录在中 https://golang.org/pkg/net/http/ 并包含 HTTP 和 HTTPS 实用程序。首先,我建议您远离社区 HTTP 框架,坚持使用 Go 标准库。标准 HTTP 包包括侦听、路由和模板功能。内置的 HTTP 服务器具有生产质量,它直接绑定到一个端口,不需要单独的 httpd,如 Apache、IIS 或 nginx。然而,nginx 在公共端口80上侦听,并反向代理在本地端口80以外侦听的所有 Go 服务器请求,这是很常见的。

在本章中,我们将介绍运行 HTTP 服务器、使用 HTTPS、设置安全 cookie 和转义输出的基础知识。我们还将介绍如何使用 Negroni 中间件包并实现用于日志记录、添加安全 HTTP 头和服务静态文件的自定义中间件。Negroni 采用惯用的 Go 方法,鼓励使用标准库net/http处理程序。它非常轻量级,构建在现有 Go 结构之上。此外,还提到了与运行 web 应用相关的其他最佳实践。

还提供了 HTTP 客户端示例。从发出基本 HTTP 请求开始,我们继续发出 HTTPS 请求,并使用客户端证书进行身份验证,使用代理进行路由通信。

在本章中,我们将介绍以下主题:

  • HTTP servers
  • 简单 HTTP 服务器
  • TLS 加密 HTTP(HTTPS)
  • 使用安全 cookies
  • HTML 转义输出
  • 内格罗尼中间件
  • 日志记录请求
  • 添加安全 HTTP 头
  • 提供静态文件
  • 其他最佳做法
  • 跨站点请求伪造(CSRF)令牌
  • Preventing user enumeration and abuse
  • 避免本地和远程文件包含漏洞
  • HTTP 客户端
  • 发出基本 HTTP 请求
  • 使用客户端 SSL 证书
  • 使用代理
  • 使用系统代理
  • Using an HTTP proxy
  • 使用 SOCKS5 代理(Tor)

HTTP server

HTTP 是建立在 TCP 层之上的应用协议。概念比较简单,;您可以使用纯文本创建请求。在第一行中,您将提供方法,例如GETPOST,以及您遵循的路径和 HTTP 版本。之后,您将提供一系列键和值对来描述您的请求。通常,您需要提供一个Host值,以便服务器知道您正在请求哪个网站。一个简单的 HTTP 请求可能如下所示:

GET /archive HTTP/1.1
Host: www.devdungeon.com  

不过,您不必担心 HTTP 规范中的所有细节。Go 提供了一个net/http包,该包附带了一些工具,用于轻松创建可用于生产的 web 服务器,包括支持带有 Go 1.6 和更新版本的 HTTP/2.0。本节介绍与运行和保护 HTTP 服务器相关的主题。

简单 HTTP 服务器

在本例中,HTTP 服务器演示了使用标准库创建侦听服务器是多么简单。目前还没有路由或多路复用。在这种情况下,通过服务器提供特定目录。http.FileServer()内置了目录列表,因此如果您向/发出 HTTP 请求,那么它将列出所服务目录中可用的文件:

package main

import (
   "fmt"
   "log"
   "net/http"
   "os"
)

func printUsage() {
   fmt.Println(os.Args[0] + ` - Serve a directory via HTTP

URL should include protocol IP or hostname and port separated by colon.

Usage:
  ` + os.Args[0] + ` <listenUrl> <directory>

Example:
  ` + os.Args[0] + ` localhost:8080 .
  ` + os.Args[0] + ` 0.0.0.0:9999 /home/nanodano
`)
}

func checkArgs() (string, string) {
   if len(os.Args) != 3 {
      printUsage()
      os.Exit(1)
   }
   return os.Args[1], os.Args[2]
}

func main() {
   listenUrl, directoryPath := checkArgs()
   err := http.ListenAndServe(listenUrl,      
     http.FileServer(http.Dir(directoryPath)))
   if err != nil {
      log.Fatal("Error running server. ", err)
   }
}

下一个示例演示如何路由路径和创建函数来处理传入请求。此程序不接受任何命令行参数,因为它本身不是一个非常有用的程序,但您可以将其用作基本模板:

package main

import (
   "fmt"
   "net/http"
   "log"
)

func indexHandler(writer http.ResponseWriter, request *http.Request) {
   // Write the contents of the response body to the writer interface
   // Request object contains information about and from the client
   fmt.Fprintf(writer, "You requested: " + request.URL.Path)
}

func main() {
   http.HandleFunc("/", indexHandler)
   err := http.ListenAndServe("localhost:8080", nil)
   if err != nil {
      log.Fatal("Error creating server. ", err)
   }
}

HTTP 基本身份验证

HTTP basic auth 采用用户名和密码,将它们与冒号分隔符组合,并使用 base64 对它们进行编码。用户名和密码通常可以作为 URL 的一部分传递,例如:http://<username>:<password>@www.example.com。在后台,用户名和密码被组合、编码并作为 HTTP 头传递。

如果使用这种身份验证方法,请记住它不是加密的。传输过程中没有对用户名和密码的保护。您总是希望在传输层上使用加密,这意味着添加 TLS/SSL。

HTTP 基本身份验证现在没有得到广泛应用,但它很容易实现。一种更常见的方法是在应用中构建或使用您自己的身份验证层,例如将用户名和密码与一个用户数据库进行比较,该数据库中充满了盐渍和哈希密码。

参见第 8 章暴力,创建客户端并连接到需要 HTTP 基本认证的 HTTP 服务器的示例。Go 标准库仅为作为客户端的 HTTP basic auth 提供了一种方法。它不提供在服务器端检查基本身份验证的方法。

我不建议您再在服务器上实现 HTTP basic auth。如果需要对客户端进行身份验证,请使用 TLS 证书。

Using HTTPS

第 6 章加密中,我们指导您完成生成密钥所需的步骤,然后创建您自己的自签名证书。我们还提供了一个如何运行 TCP 套接字级别 TLS 服务器的示例。本节将演示如何创建 TLS 加密的 HTTP 服务器或 HTTPS 服务器。

TLS 是 SSL 的较新版本,Go 有一个很好地支持它的标准包。您需要一个私钥和用该密钥生成的签名证书。您可以使用自签名证书或由公认的证书颁发机构签名的证书。从历史上看,由可信机构签署的 SSL 证书总是要花钱的,但是https://letsencrypt.org/ 改变了游戏规则,他们开始提供由广受信任的权威机构签署的免费自动证书。

如果本例需要证书(cert.pem),请参考第 6 章密码,了解创建您自己的自签名证书的示例。

下面的代码演示了如何运行为单个网页提供服务的 HTTPS 服务器的最基本示例。有关各种 HTTP 蜜罐示例和更多 HTTP 服务器参考代码,请参考第 10 章网页抓取中的示例。在源代码中初始化 HTTPS 服务器后,可以使用与处理 HTTP 服务器对象相同的方法来处理它。请注意,此服务器与 HTTP 服务器之间的唯一区别是调用了http.ListenAndServeTLS()而不是http.ListenAndServe()。此外,您必须提供服务器的证书和密钥:

package main

import (
   "fmt"
   "net/http"
   "log"
)

func indexHandler(writer http.ResponseWriter, request *http.Request) {
   fmt.Fprintf(writer, "You requested: "+request.URL.Path)
}

func main() {
   http.HandleFunc("/", indexHandler)
   err := http.ListenAndServeTLS( 
      "localhost:8181", 
      "cert.pem", 
      "privateKey.pem", 
      nil, 
   )
   if err != nil {
      log.Fatal("Error creating server. ", err)
   }
}

创建安全 cookie

Cookie 本身不应该包含用户无法看到的敏感信息。攻击者可以以 Cookie 为目标,尝试收集私人信息。最常见的目标是会话 cookie。如果会话 cookie 被破坏,攻击者可以使用 cookie 模拟用户,服务器会允许它。

The HttpOnly flag asks the browser to prevent JavaScript from accessing the cookie, protecting against cross-site scripting attacks. The cookie will only get sent when making HTTP requests. If you do need a cookie to be accessed via JavaScript, just create a different cookie from the session cookie.

Secure标志要求浏览器仅传输具有 TLS/SSL 加密的 cookie。这可以防止会话侧劫持尝试,通常通过嗅探未加密的公共 Wi-Fi 网络或中间人连接来实现。一些网站只会在登录页面上放置 SSL 以保护您的密码,但之后的每个连接都是在纯 HTTP 中完成的,会话 cookie 可能会在线路上被盗,或者,如果缺少HttpOnly标志,则可能会使用 JavaScript。

创建会话令牌时,请确保它是使用加密安全的伪随机数生成器生成的。会话令牌应至少为 128 位。生成安全随机字节的示例请参见第 6 章密码

下面的示例创建了一个简单的 HTTP 服务器,它只有一个函数indexHandler()。该函数使用建议的安全设置创建 cookie,然后在打印响应正文并返回之前调用http.SetCookie()

package main

import (
   "fmt"
   "net/http"
   "log"
   "time"
)

func indexHandler(writer http.ResponseWriter, request *http.Request) {
   secureSessionCookie := http.Cookie {
      Name: "SessionID",
      Value: "<secure32ByteToken>",
      Domain: "yourdomain.com",
      Path: "/",
      Expires: time.Now().Add(60 * time.Minute),
      HttpOnly: true, // Prevents JavaScript from accessing
      Secure: true, // Requires HTTPS
   }   
   // Write cookie header to response
   http.SetCookie(writer, &secureSessionCookie)   
   fmt.Fprintln(writer, "Cookie has been set.")
}

func main() {
   http.HandleFunc("/", indexHandler)
   err := http.ListenAndServe("localhost:8080", nil)
   if err != nil {
      log.Fatal("Error creating server. ", err)
   }
}

HTML 转义输出

Go 有一个标准函数来转义字符串并防止 HTML 字符被渲染。

将用户接收到的任何数据输出到响应输出时,始终对其进行转义,以防止跨站点脚本攻击。这同样适用于用户提供的数据是否来自 URL 查询、POST 值、用户代理头、表单、cookie 或数据库。以下代码段给出了转义字符串的示例:

package main

import (
   "fmt"
   "html"
)

func main() {
   rawString := `<script>alert("Test");</script>`
   safeString := html.EscapeString(rawString)

   fmt.Println("Unescaped: " + rawString)
   fmt.Println("Escaped: " + safeString)
}

内格罗尼中间件

中间件是指可以绑定到请求/响应流并在将其传递到下一个中间件并最终返回到客户端之前采取行动或进行修改的功能。

中间件是在每个请求上按顺序运行的一系列功能。您可以向该链添加更多函数。我们将看一些实际的例子,例如将 IP 地址列入黑名单、添加日志记录和添加授权检查。

中间件的顺序很重要。例如,我们可能希望首先放置日志中间件,然后放置 IP 黑名单中间件。我们希望 IP 黑名单模块首先运行,或者至少在最开始的时候运行,这样其他中间件就不会浪费资源来处理一个无论如何都会被拒绝的请求。您可以在将请求和响应传递给下一个中间件处理程序之前对其进行操作。

您可能还希望为分析、日志记录、黑名单 IP 地址、注入头或拒绝某些用户代理(如curlpythongo)构建自定义中间件。

这些示例使用 Negroni 包。在编译和运行这些示例之前,您需要go get包。这些示例称为http.ListenAndServe(),但您可以同样轻松地修改它们以将 TLS 与http.ListenAndServeTLS()一起使用:

go get github.com/urfave/negroni 

下面的示例创建了一个customMiddlewareHandler()函数,我们将告诉negroniHandler接口使用它。自定义中间件只记录传入的请求 URL 和用户代理,但您可以做任何您喜欢的事情,包括在请求返回到客户端之前修改请求:

package main

import (
   "fmt"
   "log"
   "net/http"

   "github.com/urfave/negroni"
)

// Custom middleware handler logs user agent
func customMiddlewareHandler(rw http.ResponseWriter, 
   r *http.Request, 
   next http.HandlerFunc, 
) {
   log.Println("Incoming request: " + r.URL.Path)
   log.Println("User agent: " + r.UserAgent())

   next(rw, r) // Pass on to next middleware handler
}

// Return response to client
func indexHandler(writer http.ResponseWriter, request *http.Request) {
   fmt.Fprintf(writer, "You requested: " + request.URL.Path)
}

func main() {
   multiplexer := http.NewServeMux()
   multiplexer.HandleFunc("/", indexHandler)

   negroniHandler := negroni.New()
   negroniHandler.Use(negroni.HandlerFunc(customMiddlewareHandler))
   negroniHandler.UseHandler(multiplexer)

   http.ListenAndServe("localhost:3000", negroniHandler)
}

日志记录请求

由于日志记录是一项非常常见的任务,因此 Negroni 附带了一个可以使用的日志记录中间件,如下例所示:

package main

import (
   "fmt"
   "net/http"

   "github.com/urfave/negroni"
)

// Return response to client
func indexHandler(writer http.ResponseWriter, request *http.Request) {
   fmt.Fprintf(writer, "You requested: " + request.URL.Path)
}

func main() {
   multiplexer := http.NewServeMux()
   multiplexer.HandleFunc("/", indexHandler)

   negroniHandler := negroni.New()
   negroniHandler.Use(negroni.NewLogger()) // Negroni's default logger
   negroniHandler.UseHandler(multiplexer)

   http.ListenAndServe("localhost:3000", negroniHandler)
}

添加安全 HTTP 头

利用 Negroni 包,我们可以轻松创建自己的中间件来注入一组 HTTP 头,以帮助提高安全性。您需要评估每个标题,以查看它是否对您的应用有意义。此外,并非每个浏览器都支持这些标题中的每一个。这是一个很好的基线,可以根据您的需要进行修改。

The following headers are used in this example:

| 表头 | 说明 | | Content-Security-Policy | 这定义了哪些脚本或远程主机是可信的,并且能够提供可执行的 JavaScript | | X-Frame-Options | 这定义了是否可以使用 frames 和 iframe,以及允许哪些域出现在 frames 中 | | X-XSS-Protection | 这会告诉浏览器,如果检测到跨站点脚本攻击,则停止加载;如果定义了良好的Content-Security-Policy标题,则基本上没有必要这样做 | | Strict-Transport-Security | 这告诉浏览器只使用 HTTPS 而不使用 HTTP | | X-Content-Type-Options | 这告诉浏览器使用服务器提供的 MIME 类型,而不是根据 MIME 嗅探的猜测进行修改 |

是否使用或忽略这些标题最终取决于客户端的 web 浏览器。如果没有知道如何应用标题值的浏览器,它们无法保证任何安全性。

本例创建了一个名为addSecureHeaders()的函数,该函数用作附加的中间件处理程序,用于在响应返回到客户端之前修改响应。根据应用的需要调整标题:

package main

import (
   "fmt"
   "net/http"

   "github.com/urfave/negroni"
)

// Custom middleware handler logs user agent
func addSecureHeaders(rw http.ResponseWriter, r *http.Request, 
   next http.HandlerFunc) {
   rw.Header().Add("Content-Security-Policy", "default-src 'self'")
   rw.Header().Add("X-Frame-Options", "SAMEORIGIN")
   rw.Header().Add("X-XSS-Protection", "1; mode=block")
   rw.Header().Add("Strict-Transport-Security", 
      "max-age=10000, includeSubdomains; preload")
   rw.Header().Add("X-Content-Type-Options", "nosniff")

   next(rw, r) // Pass on to next middleware handler
}

// Return response to client
func indexHandler(writer http.ResponseWriter, request *http.Request) {
   fmt.Fprintf(writer, "You requested: " + request.URL.Path)
}

func main() {
   multiplexer := http.NewServeMux()
   multiplexer.HandleFunc("/", indexHandler)

   negroniHandler := negroni.New()

   // Set up as many middleware functions as you need, in order
   negroniHandler.Use(negroni.HandlerFunc(addSecureHeaders))
   negroniHandler.Use(negroni.NewLogger())
   negroniHandler.UseHandler(multiplexer)

   http.ListenAndServe("localhost:3000", negroniHandler)
}

提供静态文件

另一个常见的 web 服务器任务是为静态文件提供服务。值得一提的是用于服务静态文件的 Negroni 中间件处理程序。只需添加一个额外的Use()呼叫并传递negroni.NewStatic()即可。确保静态文件目录只包含客户端应该访问的文件。在大多数情况下,静态文件目录包含客户端的 CSS 和 JavaScript 文件。不要放置数据库备份、配置文件、SSH 密钥、Git 存储库、开发文件或任何客户端无权访问的内容。添加静态文件中间件,如下所示:

negroniHandler.Use(negroni.NewStatic(http.Dir("/path/to/static/files")))  

其他最佳做法

在创建 web 应用时,还有一些其他事情值得考虑。虽然它们不是具体的,但在开发时值得考虑这些最佳实践。

CSRF 代币

Cross-Site Request Forgery, or CSRF, tokens are a way of trying to prevent one website from taking action on your behalf against a different website.

CSRF 是一种常见的攻击,受害者访问的网站中嵌入恶意代码试图向其他网站发出请求。例如,恶意参与者嵌入 JavaScript,向每个试图将 1000 美元转移到攻击者银行帐户的银行网站发出 POST 请求。如果受害者与其中一家银行有活动会话,而该银行未实现 CSRF 代币,则该银行的网站可能会接受该请求并对其进行处理。

如果受信任站点易受反射或存储的跨站点脚本攻击,则即使在受信任站点上,也可能成为 CSRF 攻击的受害者。自 2007 年以来,CSRF 一直位列OWASP 前 10名,并在 2017 年保持不变。

Go 提供了一个xsrftoken套餐,您可以在上阅读更多相关信息 https://godoc.org/golang.org/x/net/xsrftoken 。它提供了创建令牌的Generate()函数和验证令牌的Valid()函数。您可以使用它们的实现来选择开发自己的,以满足您的需要。

要实现 CSRF 令牌,请创建一个 16 字节的随机令牌,并将其存储在与用户会话相关联的服务器上。您可以使用任何后端来存储令牌,无论是在内存中、数据库中还是文件中。将 CSRF 令牌作为隐藏字段嵌入表单中。在服务器端处理表单时,请验证 CSRF 令牌是否存在并与用户匹配。使用令牌后销毁该令牌。不要重复使用相同的令牌。

实现 CSRF 令牌的各种要求已在前面的章节中介绍:

  • 生成令牌:在第 6 章加密中,标题为*加密安全伪随机数生成器(CSPRNG)*的章节提供了生成随机数、字符串和字节的示例。
  • 创建、服务和处理 HTML 表单:在第 9 章Web 应用中,标题为HTTP 服务器的部分提供了有关创建安全 Web 服务器的信息,第 12 章社会工程中有一个标题为的部分 HTTP POST 表单登录蜜罐有一个处理 POST 请求的示例。
  • 将令牌存储在文件中:在第 3 章处理文件中,标题为将字节写入文件的章节提供了将数据存储在文件中的示例。
  • 在数据库中存储令牌:在第 8 章暴力中,标题为暴力数据库登录的章节提供了连接各种数据库类型的蓝图。

防止用户枚举和滥用

这里需要记住的重要事项如下:

  • 不要让人们知道谁有账户
  • 不要让别人用你的电子邮件服务器向你的用户发送垃圾邮件
  • 不要让人们知道谁是通过暴力企图注册的

让我们详细说明一下实际例子。

登记

当有人试图注册电子邮件地址时,不要向 web 客户端用户提供有关帐户是否已注册的任何反馈。相反,向该地址发送一封电子邮件,并简单地向 web 用户发送一条消息,说明“已向提供的地址发送了一封电子邮件。”

如果他们从未注册过,一切正常。如果他们已经注册,则不会通知 web 用户电子邮件已经注册。相反,会向用户的地址发送一封电子邮件,通知他们该电子邮件已注册。这将提醒他们已经拥有帐户,他们可以使用密码重置工具,或者让他们知道有可疑的事情,有人可能在做恶意的事情。

请小心,不要让攻击者重复尝试登录过程,并向真实用户生成大量电子邮件。

登录

不要向 web 用户反馈电子邮件是否存在。您不希望有人能够尝试使用电子邮件地址登录,并通过返回的错误消息了解该地址是否有帐户。例如,攻击可能试图使用电子邮件地址列表登录,如果 web 服务器返回“该密码不匹配”,对于某些电子邮件返回“该密码不匹配”,对于其他电子邮件返回“该电子邮件未注册”,则他们可以确定哪些电子邮件已向您的服务注册。

重置密码

避免允许垃圾邮件。对发送的电子邮件进行速率限制,以便攻击者无法通过多次提交忘记密码表单向您的用户发送垃圾邮件。

创建重置令牌时,请确保其具有良好的熵,以便无法猜测。不要仅仅根据时间和用户 ID 创建令牌,因为这很容易被猜测和强制,因为它缺少足够的熵。您应该为令牌使用至少 16-32 个随机字节以获得适当的熵。参考第 6 章加密,生成加密安全随机字节的示例。

另外,将令牌设置为在短时间后过期。根据您的应用,一小时到一天都是不错的选择。一次只允许一个重置令牌,并在使用令牌后销毁该令牌,使其无法重新播放和使用。

用户配置文件

与登录页面类似,如果您有用户配置文件页面,请注意允许用户名枚举。例如,如果有人访问/users/JohnDoe然后访问/users/JaneDoe,其中一个返回404 Not Found错误,而另一个返回401 Access Denied错误,攻击者可以推断一个帐户实际存在,而另一个不存在。

防止 LFI 和 RFI 滥用

本地文件包含****LFI远程文件包含****RFI是其他OWASP 前 10漏洞。它们指的是从本地文件系统或远程主机加载不打算加载的文件的危险,或加载预期文件但包含受污染的数据的危险。远程文件包含是危险的,因为如果不采取预防措施,用户可能会从恶意服务器提供远程文件。

如果文件名由用户指定而未经任何清理,则不要从本地文件系统打开文件。考虑一个示例,其中 Web 文件在请求时由 Web 服务器返回。用户可以使用如下 URL 请求具有敏感系统信息的文件,例如/etc/passwd

http://localhost/displayFile?filename=/etc/passwd  

如果 web 服务器这样处理,这可能是一个大问题(伪代码):

file = os.Open(request.GET['filename'])
return file.ReadAll()

您不能简单地通过预先设置一个特定目录来修复它,如下所示:

os.Open('/path/to/mydir/' + GET['filename']).

这还不够,因为攻击者可以使用目录遍历返回文件系统的根目录,如下所示:

http://localhost/displayFile?filename=../../../etc/passwd   

确保检查包含任何文件的目录遍历攻击。

受污染的文件

如果攻击者发现 LFI,或者您提供了日志文件的 web 界面,则需要确保即使日志受到污染,也不会执行任何代码。

攻击者可以通过对您的服务执行某些创建日志项的操作来潜在地污染您的日志并插入恶意代码。必须考虑生成已加载或显示的日志的任何服务。

例如,web 服务器日志可能会因向实际上是代码的 URL 发出 HTTP 请求而受到污染。您的日志将有一个404 Not Found错误,并记录请求的 URL,这实际上是代码。如果是 PHP 服务器或其他脚本语言,这将打开潜在的代码执行,但使用 Go,最糟糕的情况是 JavaScript 注入,这对用户来说仍然是危险的。设想一个场景,其中 web 应用有一个 HTTP 日志查看器,可以从磁盘加载日志文件。如果攻击者向yourwebsite.com/<script>alert("test");</script>发出请求,则如果未对代码进行转义或正确清理,您的 HTML 日志查看器最终可能会呈现该代码。

HTTP 客户端

发出 HTTP 请求是当今许多应用的核心部分。Go 是一种对 web 友好的语言,它在net/http包中包含了几个用于发出 HTTP 请求的工具。

基本 HTTP 请求

本例使用来自net/http标准库包的http.Get()函数。它将整个响应体读取到一个名为body的变量,然后将其打印到标准输出:

package main

import (
   "fmt"
   "io/ioutil"
   "log"
   "net/http"
)

func main() {
   // Make basic HTTP GET request
   response, err := http.Get("http://www.example.com")
   if err != nil {
      log.Fatal("Error fetching URL. ", err)
   }

   // Read body from response
   body, err := ioutil.ReadAll(response.Body)
   response.Body.Close()
   if err != nil {
      log.Fatal("Error reading response. ", err)
   }

   fmt.Printf("%s\n", body)
}

使用客户端 SSL 证书

如果远程 HTTPS 服务器具有严格的身份验证,并且需要受信任的客户端证书,则可以通过在http.Client用于发出 GET 请求的http.Transport对象中设置TLSClientConfig变量来指定证书文件。

此示例生成的 HTTP GET 请求与前一示例类似,但它不使用net/http包提供的默认 HTTP 客户端。它创建一个自定义http.Client并将其配置为使用 TLS 和客户端证书。如果您需要证书或私钥,请参考第 6 章加密,获取生成密钥和自签名证书的示例:

package main

import (
   "crypto/tls"
   "log"
   "net/http"
)

func main() {
   // Load cert
   cert, err := tls.LoadX509KeyPair("cert.pem", "privKey.pem")
   if err != nil {
      log.Fatal(err)
   }

   // Configure TLS client
   tlsConfig := &tls.Config{
      Certificates: []tls.Certificate{cert},
   }
   tlsConfig.BuildNameToCertificate()
   transport := &http.Transport{ 
      TLSClientConfig: tlsConfig, 
   }
   client := &http.Client{Transport: transport}

   // Use client to make request.
   // Ignoring response, just verifying connection accepted.
   _, err = client.Get("https://example.com")
   if err != nil {
      log.Println("Error making request. ", err)
   }
}

使用代理

前向代理在很多方面都很有用,包括查看 HTTP 流量、调试应用、反向工程 API 和操纵头,并且它可能用于增加对目标服务器的匿名性。但是,请注意,许多代理服务器仍然使用X-Forwarded-For头转发您的原始 IP。

您可以使用环境变量设置代理,也可以在请求中显式设置代理。Go HTTP 客户端支持 HTTP、HTTPS 和 SOCKS5 代理,如 Tor。

使用系统代理

如果通过环境变量设置,Go 的默认 HTTP 客户端将尊重系统的 HTTP(s)代理。Go 使用HTTP_PROXYHTTPS_PROXYNO_PROXY环境变量。小写版本也有效。您可以在运行流程之前设置环境变量,或在 Go 中设置环境变量,并执行以下操作:

os.Setenv("HTTP_PROXY", "proxyIp:proxyPort")  

配置环境变量后,使用默认 Go HTTP 客户端发出的任何 HTTP 请求都将遵守代理设置。在上阅读有关默认代理设置的更多信息 https://golang.org/pkg/net/http/#ProxyFromEnvironment

使用特定的 HTTP 代理

要显式设置代理 URL,忽略环境变量,请在http.Client使用的自定义http.Transport对象中设置ProxyURL变量。下面的示例创建自定义http.Transport并指定proxyUrlString。该示例仅具有代理的占位符值,必须替换为有效代理。然后创建http.Client并将其配置为与代理一起使用自定义传输:

package main

import (
   "io/ioutil"
   "log"
   "net/http"
   "net/url"
   "time"
)

func main() {
   proxyUrlString := "http://<proxyIp>:<proxyPort>"
   proxyUrl, err := url.Parse(proxyUrlString)
   if err != nil {
      log.Fatal("Error parsing URL. ", err)
   }

   // Set up a custom HTTP transport for client
   customTransport := &http.Transport{ 
      Proxy: http.ProxyURL(proxyUrl), 
   }
   httpClient := &http.Client{ 
      Transport: customTransport, 
      Timeout:   time.Second * 5, 
   }

   // Make request
   response, err := httpClient.Get("http://www.example.com")
   if err != nil {
      log.Fatal("Error making GET request. ", err)
   }
   defer response.Body.Close()

   // Read and print response from server
   body, err := ioutil.ReadAll(response.Body)
   if err != nil {
      log.Fatal("Error reading body of response. ", err)
   }
   log.Println(string(body))
}

使用 SOCKS5 代理(Tor)

Tor 是一种匿名服务,试图保护您的隐私。除非您完全理解所有含义,否则不要使用 Tor。更多关于 Tor 的信息,请访问https://www.torproject.org 。本例演示了在发出请求时如何使用 Tor,但这同样适用于其他 SOCKS5 代理。

要使用 SOCKS5 代理,只需修改代理的 URL 字符串。不要使用 HTTP 协议,而是使用socks5://协议前缀。

使用 Tor 浏览器包时,默认 Tor 端口为90509150。以下示例将对check.torproject.org执行 GET 请求,这将让您知道您是否正确地通过 Tor 网络路由:

package main

import (
   "io/ioutil"
   "log"
   "net/http"
   "net/url"
   "time"
)

// The Tor proxy server must already be running and listening
func main() {
   targetUrl := "https://check.torproject.org"
   torProxy := "socks5://localhost:9050" // 9150 w/ Tor Browser

   // Parse Tor proxy URL string to a URL type
   torProxyUrl, err := url.Parse(torProxy)
   if err != nil {
      log.Fatal("Error parsing Tor proxy URL:", torProxy, ". ", err)
   }

   // Set up a custom HTTP transport for the client   
   torTransport := &http.Transport{Proxy: http.ProxyURL(torProxyUrl)}
   client := &http.Client{
      Transport: torTransport,
      Timeout: time.Second * 5
   }

   // Make request
   response, err := client.Get(targetUrl)
   if err != nil {
      log.Fatal("Error making GET request. ", err)
   }
   defer response.Body.Close()

   // Read response
   body, err := ioutil.ReadAll(response.Body)
   if err != nil {
      log.Fatal("Error reading body of response. ", err)
   }
   log.Println(string(body))
}

总结

在本章中,我们介绍了运行用 Go 编写的 web 服务器的基础知识。您现在应该可以轻松地创建一个基本的 HTTP 和 HTTPS 服务器了。此外,您应该了解中间件的概念,并了解如何使用 Negroni 包实现预构建和自定义中间件。

我们还介绍了在尝试保护 web 服务器时的一些最佳实践。您应该了解什么是 CSRF 攻击,以及如何预防它。您应该能够解释本地和远程文件包含以及风险是什么。

标准库中的 web 服务器具有生产质量,它拥有创建生产就绪 web 应用所需的一切。web 应用还有许多其他框架,如 Gorilla、Revel 和 Martini,但最终,您必须评估每个框架提供的功能,看看它们是否符合您的项目需求。

我们还介绍了标准库提供的 HTTP 客户机函数。您应该知道如何使用客户端证书发出基本 HTTP 请求和经过身份验证的请求。您应该了解在发出请求时如何使用 HTTP 代理。

在下一章中,我们将探索 web 抓取,以从 HTML 格式的网站中提取信息。我们将从基本技术开始,如字符串匹配和正则表达式,并探索使用 HTMLDOM 的goquery包。我们还将介绍如何使用 cookies 在登录会话中爬行。还讨论了识别框架的指纹 web 应用。我们还将介绍使用广度优先和深度优先的方法在 web 上爬行。