零基础学go—GO黑帽子学习笔记-4.1 HTTP服务器基础
2022-10-20 10:57:5 Author: 藏剑安全(查看原文) 阅读量:26 收藏

一个简单的服务器

package main

import (
"fmt"
"net/http"
)

func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello %s", r.URL.Query().Get("name"))
}

func main() {
http.HandleFunc("/hello", hello)
http.ListenAndServe(":8000", nil)

如上代码,我们便可以启动一个简单的http服务器。在这个简单的示例中,我们首先来看main函数,我们先调用http.HandlerFunc接收两个参数,一个是服务器的路径,一个是该路径使用的处理函数。然后调用http.ListenAndServe启动服务对端口进行监听。而在hello方法中,我们获取来Get请求的参数name,并将其利用fmt.Fprintf将格式化后的词付出传递给我们的http.ResponseWriter,该参数是一个io.writer类型的参数,可以将传入的内容打印到网页中,而另一个参数http.Request可以用来获得请求中的一些信息。

那么实际上HandleFunc方法在底层上是如何运行的呢?由go的文档可知,处理程序被放置在DefaultServerMux上。ServerMux是多路复用器的简写。可以这样理解它,基础代码可以处理针对模式和函数的多个HTTP请求。并且它由线程进行执行。导入net/http包并创建一个ServerMux,然后将其附加到该包的名称空间,这就是DeautifultServerMux。

然后对于ListenAndServer()函数来说,它可以调用以字符串和http.Handler作为参数。第一个参数为启动HTTP服务器的地址。而第二个参数http.Handler,传入的是nil的情况下,程序包会将DefaultServerMux作为基础处理程序使用。关于http.Handler,我们下面的代码中将会将其实现。当然,我们也可以使用http.ListenAndServeTLS()来实现HTTPS和TLS服务器,但是需要其他参数。

构建一个简单的路由器

package main

import (
"fmt"
"net/http"
)

type router struct {
}

func (r *router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
switch req.URL.Path {
case "/a":
fmt.Fprint(w, "executing /a")
case "/b":
fmt.Fprint(w, "executing /b")
case "/c":
fmt.Fprint(w, "executing /c")
default:
http.Error(w, "404 Not Found", 404)
}
}

func main() {
var r router
http.ListenAndServe(":8000", &r)
}

在如上代码中,我们构建了一个简单的路由器,在访问路径/a、/b、/c等页面时会打印对应的结果,而访问其他页面时则提示404。我们接下来来看具体的实现。

首先我们实现了一个空的结构体router,该结构体是为了下面的方法而创建(类似于其他语言中创建了一个类),我们将使用这个结构体来实现接口http.Handler。为此,我们必须定义方法ServeHTTP,注意没有s不是ServerHTTP,然后传入两个参数,并在其中利用switch实现对不同路径的不同处理。

上面提到了,我们http.ListenAndServe的第二个参数为http.Hadnler的指针,也就是我们创建的router的指针传入。

构建简单的中间件

package main

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

type logger struct {
Inner http.Handler
}

func (l *logger) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log.Println("start")
l.Inner.ServeHTTP(w, r)
log.Println("finish")
}

func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello\n")
}

func main() {
f := http.HandlerFunc(hello)
l := logger{Inner: f}
http.ListenAndServe(":8000", &l)
}

在我们实际的网站中,我们往往需要使用各种中间件,他们可以帮助我们快速实现许多功能。而中间件简单来说,是一种包装程序,它将在所有传入请求上传执行,而与目标函数无关。在上述代码中,我们创建了一个记录器,该记录器将显示有关请求处理的开始和结束时间。

我们实际要做的是创建一个外部处理程序,该处理程序会在每次请求时都会在服务器上记录一些信息并调用函数hello()。我们将此日志逻辑包含在函数中。

和在路由器中一样,我们首先也构建一个名为logger的结构体,但是这次有一个字段Inner,它本身是一个接口http.Handler()。在ServerHTTP()定义中,使用log()打印请求的开始和结束时间,并且在两者之间调用内部处理程序方法ServeHTTP()。然后继续我们创建服务器的步骤,完成一个hello页面。

然后我们在main函数中,首先使用HandlerFunc方法从函数中创建接口http.Handler,然后创建结构体,并利用其开启HTTP服务。

使用gorilla/mux包进行路由

go的生态中,提供了多个第三方路由器。在这里我们首先使用gorilla/mux包。而gorilla/mux可以基于简单和复杂模式进行路由。它还包含正则表达式、参数匹配、动词匹配和子路由以及其他功能。

下面我们来学习这个包的使用。

首先我们可以使用get命令获取gorilla/mux包。

go get github.com/gorilla/mux

我们首先在11行利用mux.NewRouter()创建路由器,返回的类型实现了接口http.Handler,但是同时也需要具有许多其他关联的方法。其中最长使用的是HandleFunc。在12行,如果想定义新的路由来处理对/foo模式的GET请求,则可使用该方法。由于使用了Methods("GET"),所以只有GET请求才会匹配该路由。所以其他方法将返回404响应。

然后在15行我们再次利用HandleFunc实现一个路由,在这其中,我们通过加上{}来定义请求参数,可以将此视为已命名的占位符。然后在处理函数汇总调用mu x.Vars()来将请求对象传递给他,此时会返回map[string] string,它是请求参数名称到他们各自值的映射。由于命名的占位符user作为映射的键,因此对/users/bob的请求会响应hi bob。并且我们在{}中使用了正则表达式来限定传递的参数,如代码中我们限定必须为小写字母。

最后我们还是通过ListenAndServe来创建监听服务器。这里是不传递指针的,因为我们使用mux.NewRouter()来创建的路由器返回的就是一个http.Handler的指针。

package main

import (
"fmt"
"net/http"

"github.com/gorilla/mux"
)

func main() {
r := mux.NewRouter()
r.HandleFunc("/foo", func(w http.ResponseWriter, req *http.Request) {
fmt.Fprint(w, "hi foo")
}).Methods("GET")
r.HandleFunc("/users/{user:[a-z]+}", func(w http.ResponseWriter, req *http.Request) {
user := mux.Vars(req)["user"]
fmt.Fprintf(w, "hi %s", user)
}).Methods("GET")
http.ListenAndServe(":8000", r)
}

使用negroni包构建中间件

在上面我们使用一个简单的中间件记录了有关请求的开始和结束时间,并且返回响应。但是在大多数情况下,中间件不必对每个传入请求都进行操作。使用中间件的原因很多,其中包括记录请求、对用户进行身份验证和授权来映射资源。

比如,我们可以编写用于执行基本身份验证的中间件。它可以为每个请求解析一个授权标头,验证所提供的用户名和密码,如果凭证是无效的,则返回401响应。我们还可以将多个中间件链接在一起,从而能在执行一个中间件之后运行下一个中间件。

此前创建的日志记录中间件仅包装了一个函数,实际上,这基本没什么用,因为一般需要使用多个函数,而且要执行此操作,必须具有可以在一个链中一个接一个地执行它们的逻辑。所以这里我们要使用negroni包。

而negroni包,它不会将我们束缚在一个大的框架中,因此非常有用。我们可以轻松地将其栓接到其他框架上。它提供了很大的灵活性,还带有很多应用程序都会用到的默认中间件。

go get github.com/urfave/negroni

从技术上讲,可以讲negroni包用于所有应用程序逻辑,但这并不是理想的选择,因为该包只能充当中间件,并不包含路由器。不过,可以将negroni包与其他包(例如gorilla/mux或net/http包)搭配起来使用。在这里,我们使用gorilla/mux包构建一个程序,来让我们可以熟悉negroni包并学会如何使用它来遍历中间件链。

package main

import (
"net/http"

"github.com/gorilla/mux"
"github.com/urfave/negroni"
)

func main() {
r := mux.NewRouter()
n := negroni.Classic()
n.UseHandler(r)
n.Use(negroni.NewRecovery())
http.ListenAndServe(":8000", n)
}

首先,我们调用mux.NewRouter()来创建路由器。接下来是与negroni包的首次交互,调用negroni.Classic()。这将创建一个指向Negroni实例的新指针。想要创建新指针,我们可以使用多种方法。negroni.Classic()使用默认的中间件,包括请求记录器、在紧急情况下进行拦截和恢复的中间件,以及服务于同一目录中公用文件夹的中间件。函数negroni.New()不会创建任何默认的中间件。

negroni包提供了各种类型的中间件,例如可以通过执行以下操作来使用恢复包。

n.Use(negroni.NewRecovery())

接下来,通过调用n.UseHandler(r)将路由器添加到中间件堆栈。在这里继续构建中间件时,要考虑执行顺序。比如我们希望身份验证检查中间件在需要身份验证的函数之前运行。在路由器之前添加的任何中间件都将在处理函数运行前执行。路由器之后添加的任何中间件都将在处理函数运行后执行。所以顺序十分重要。最后我们在这里监听端口,创建HTTP服务器。

使用默认中间件固然很有用,但是只有创建自己的中间价才能让negroni包发挥真正的作用。利用negroni包,我们有很多种方法可以将中间件添加到程序栈中。

比如在下面的代码中,我们创建了trivial类型的中间件,该中间件输出一条消息并执行传递给链中的下一个中间件。

package main

import (
"fmt"
"net/http"

"github.com/gorilla/mux"
"github.com/urfave/negroni"
)

type trivial struct {
}

func (t *trivial) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
fmt.Println("Executing trivial middleware")
next(w, r)
}

func main() {
trivial := trivial{}
r := mux.NewRouter()
n := negroni.Classic()
n.UseHandler(r)
n.Use(&trivial)
http.ListenAndServe(":8000", n)
}

中间件的构建就不多说了,我们可以在main()函数中看到,首先我们首先创建了一个路由,然后创建默认中间件,在将默认中间件加入后,我们将我们刚刚创建的中间件也加入,并开启监听。

需要注意的是,我们在这里实现的结构ServeHTTP并不是http.Handler而是negroni.Handler。二者不同的是negroni的Handler需要增加一个next http.HandlerFunc作为参数。其表示链中的下一个中间件。在方法ServeHTTP中进行处理,然后调用next()并向其传递函数http.ResponseWriter和http.Request的值,这样能有效地在中间价上转移。

然后我们使用use函数将我们刚刚创建的中间件加入。

当然,这也存在一个问题,我们无论编写什么都需要使用negroni包,但是很多程序栈都不太喜欢结构negroni.Handler。所以在非negroni程序栈中使用negroni中间件可能会出现兼容性问题。

还有两种方法可以让negroni包使用我们的中间件。一种是我们的UseHandler(handler http.Handler)。第二种是调用UseHandleFunc(handlerFunc (w http.ResponseWriter, r *http.Request))。后者不太常用,因为它不允许放弃执行链中的下一个中间价,比如你在身份验证,党身份验证不通过时通常返回401,这里就不太适合使用后者。

使用negroni包添加身份验证

我们在修改上面实现的代码,首先给路由中添加一个hello页面,他会通过get请求去获得username的参数,并将其打印。然后我们来继续完成身份验证的中间件的代码。首先还是实现negroni.Handler的接口。不同的是,此时我们在结构体中增加了用户名和密码作为参数,然后在下面的ServeHTTP方法中,首先获得GET请求中的username和password参数,然后和我们实例化时创建的用户名和密码作比对,如果错误则返回401以及错误内容,如果正确,首先使用则使用context.WithValue在上下问中设置一个名为username的变量,然后通过调用r.WithContext方法来确保请求使用新的上下文。这里需要注意的是我们在22行,也就是身份验证错误的时候增加了return,这代表如果身份验证错误,那么下面的中间件便不会被执行。

剩下的main函数中的内容与上面就大致相同了,只是我们在路由中增加了一个只能get请求的hello页面,并且将添加的自定义中间件进行了修改。

package main

import (
"context"
"fmt"
"net/http"

"github.com/gorilla/mux"
"github.com/urfave/negroni"
)

type badAuth struct {
Username string
Password string
}

func (b *badAuth) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
username := r.URL.Query().Get("username")
password := r.URL.Query().Get("password")
if username != b.Username || password != b.Password {
http.Error(w, "Unauthorized", 401)
return
}
ctx := context.WithValue(r.Context(), "username", username)
r = r.WithContext(ctx)
next(w, r)
}

func hello(w http.ResponseWriter, r *http.Request) {
username := r.Context().Value("username").(string)
fmt.Fprintf(w, "HI %s\n", username)
}

func main() {
r := mux.NewRouter()
r.HandleFunc("/hello", hello).Methods("GET")
n := negroni.Classic()
n.Use(&badAuth{
Username: "admin",
Password: "password",
})
n.UseHandler(r)
http.ListenAndServe(":8000", n)
}

使用模版生成html响应

模版使我们可以通过GO程序中的变量动态的生成内容,其中包括HTML。许多语言都有第三方包,可以用来动态生成模版,包括GO,GO有两个包,为text/template和html/template。

GO有一个妙处就是他的上下文相关性,根据变量在模版中的位置对变量进行不同的编码,比如要提供字符串作为href属性的url,则该字符串中会采用URL编码,但是如果在HTML元素中渲染该字符串,则会使用HTML编码。

要创建和使用模版,为们首先需要定义模版,其中包括一个占位符,来表示要渲染的动态上下文数据。

 package main

import (
"html/template"
"os"
)

var x = `
<html>
<body>
Hello {{.}}
</body>
</html>
`

func main() {
t, err := template.New("hello").Parse(x)
if err != nil {
panic(err)
}
t.Execute(os.Stdout, "<script>alert('world')</script>")
}

在上面的代码中我们首先创建一个名为x的变量来存储HTML模版。在这里我们使用代码中嵌入的字符串来定义模版,但是大多数情况下,需要存储模版作为单独的文件。而且在模版内部,我们可以使用{{variable-name}}约定占位符,其中variable-name是要渲染的上下文数据中的数据元素。而且变量可以使一个结构体,也可以是一个基本的数据类型。我们如果将带有字段的Username的结构体传给模版,则可以使用下面的方式渲染该字段。

{{.Username}}

接下来,在mian函数汇总,首先使用tmplate.New来创建一个新模版,调用Parse方法来保证格式化模版并正确解析。这个两个函数最终返回一个只想Template的指针。最后调用t.Execute(io.Writer,interface{})来使用。第二个参数传递的变量会处理模版并将其写入io.writer。这里其实应该是给到http.ResponseWriter,但是作者懒了。我在下一个代码中自己完成了这样的例子。当然,我们还可以通过该方法在模版中嵌入模版。

模版+negroni身份验证

package main

import (
"context"
"net/http"
"text/template"

"github.com/gorilla/mux"
"github.com/urfave/negroni"
)

type badAuth struct {
Username string
Password string
}

var x = `
<html>
<body>
Hello {{.}}
</body>
</html>
`

func (b *badAuth) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
username := r.URL.Query().Get("username")
password := r.URL.Query().Get("password")
if username != b.Username || password != b.Password {
http.Error(w, "Unauthorized", 401)
return
}
ctx := context.WithValue(r.Context(), "username", username)
r = r.WithContext(ctx)
next(w, r)
}

func hello(w http.ResponseWriter, r *http.Request) {
t, err := template.New("hello").Parse(x)
if err != nil {
panic(err)
}
t.Execute(w, "<script>alert(1)</script>")
}

func main() {
r := mux.NewRouter()
r.HandleFunc("/hello", hello).Methods("GET")
n := negroni.Classic()
n.Use(&badAuth{
Username: "admin",
Password: "password",
})
n.UseHandler(r)
http.ListenAndServe(":8000", n)
}

在上面的代码中,我们首先创建了一个参数x用来存放我们的模版,然后修改hello中的代码,利用参数x创建一个模版,然后将将模版输出给http.ResopnseWriter()。

使

零基础学go—GO黑帽子学习笔记-3.4 使用BingScraping解析文档元数据

零基础学go—GO黑帽子学习笔记-3.3 msf客户端的实现

零基础学go—GO黑帽子学习笔记-3.2 FoFa客户端实现

零基础学go GO黑帽子学习笔记-3.1 GO的HTTP基础知识


文章来源: http://mp.weixin.qq.com/s?__biz=Mzg5MDA5NzUzNA==&mid=2247484552&idx=2&sn=64ee558a8e9a57a4eeec0be0eb6179f5&chksm=cfe09e99f897178fffdde11107bb9665fe75c4f22136b4949ed927b042a5fd0401a302443943#rd
如有侵权请联系:admin#unsafe.sh