Skip to main content

Command Palette

Search for a command to run...

如何让HTTP和HTTPs监听同一个端口?

HTTP监听80端口,HTTPS监听443端口,大家都习以为常,那么让HTTPS和HTTP监听同一个端口呢?

Published
2 min read
如何让HTTP和HTTPs监听同一个端口?

如何让HTTP和HTTPS监听同一个端口?这个问题听起来很奇怪,这是因为之前在写HTTPS小故事时提到 HTTPS 并不是新的协议,而是基于HTTP协议之上封装了一层加密而已。既然都是基于HTTP协议的,那么在同一个端口同时实现HTTP和HTTPS应该也是可以的,说搞就搞。

第一步,看源码

先看了一下 Go 标准库的HTTP和HTTPS的实现,果然,HTTPS 是基于HTTP的,可以在 源码 中看到,http.ServerTLS 是使用 http.Serve 实现的,只是由 net.Listener 转为了 tls.listener,重写了 Accept 方法,然后在 tls.Server 设置了使用 tls 握手。

也就是说,我们需要在 tls 握手前判断请求是HTTP还是HTTPS的,如果是HTTPS的就升级为 tls.listener 进行 tls 握手,如果不是继续使用 net.Listener 就可以了。

image.png

那么有办法判断呢?

根据 TCP 报文判断 HTTPS

HTTPS小故事中有讲解 HTTPS认证过程,在TCP三次握手建立会话后客户端会发送Client Hello,Client Hello里有TLS相关的信息,那么我们就可以通过Client Hello是否有TLS信息判断是不是HTTPS请求。

根据HTTPS小故事文章中的参考资料,可以知道Client Hello中前5个byte是Record Header,里面记录了是否是TLS请求和TLS版本:

image.png

第一个byte是16,这里是十六进制0x16,转换十进制为22,22 表示这个报文是 TLS 握手类型,可以查看 TLS HandshakeType 了解相关内容。

第二个byte固定为0300/01/02是指TLS之前的SSL协议,已废弃,这里的03为TLS协议,所以判断是否为03就行了。

第三个byte为TLS小版本,目前 TLS 有四个小版本1.0/1.1/1.2/1.3,对应为00~03

前三个byte就够判断是否为TLS,后面的内容我们就不需要关心了,对应的信息也可以看下图更清晰:

image.png

Coding

上面分析完了,下面就编码实现吧。

启动TCP监听

要读取TCP报文首先需要启动一个TCP服务,在Go里可以通过net.Listen来启动一个TCP监听某个端口:

ln, err := net.Listen("tcp", ":6789")

如果是正常的HTTP(s)服务只需要在这个TCP监听上Serve一下就可以了,但我们需要区分HTTP和HTTPS,所以我们需要改造一下这个net.Listener

读取TCP报文

net.Listener提供了Accept方法,可以通过该方法获取net.Conn,然后从 net.Conn.Read 方法来获取 TCP 报文。我们可以自己实现一个Listener包裹一下net.Listener,然后重写Accept方法来进行覆盖,实现大概如下:

type Listener struct {
    net.Listener
}

func (ln *Listener) Accept() (net.Conn, error) {
    conn, err := ln.Listener.Accept()
    if err != nil {
        return nil, err
    }

    // 从报文取出前3个 byte 来判断是否为 HTTPS
    b := make([]byte, 3)
    _, err = conn.Read(b)
    if err != nil {
        conn.Close()
        if err != io.EOF {
            return nil, err
        }
    }

    if b[0] == 0x16 && b[1] == 0x03 && b[2] <= 0x03 {
        log.Println("HTTPS")
        return tls.Server(peekConn, &tls.Config{
            ClientAuth: tls.NoClientCert,
            GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
                cert, err := tls.LoadX509KeyPair("server.cert.pem", "server.key.pem")
                if err != nil {
                    return nil, err
                }
                return &cert, nil
            },
        }), nil
    }

    log.Println("HTTP")
    return conn, nil
}

上面就可以从TCP里读取报文来判断是否为HTTP和HTTPS了,当然 tls.Config 里还需要配置一下网站的证书,否则HTTPS访问会失败。

实际运行一下会发现报错,这时候为什么呢?

Peek Conn

我们在上面代码里读取报文使用了 conn.Read(b),这样从报文里取出来了三个byte,破坏了报文的结构,后面HTTP(S)实际处理报文的时候就会失败,所以我们需要在读取报文的时候还要保留报文的完整性,这里实现一个 PeekConn,使用 Peek 方法来读取报文:

// here's a buffered conn for peeking into the connection
type PeekConn struct {
    net.Conn
    r *bufio.Reader
}

func (c *PeekConn) Read(b []byte) (int, error) {
    return c.r.Read(b)
}

func (c *PeekConn) Peek(n int) ([]byte, error) {
    return c.r.Peek(n)
}

func newPeekConn(c net.Conn) *PeekConn {
    return &PeekConn{c, bufio.NewReader(c)}
}

修改 Accept 方法如下:

func (ln *Listener) Accept() (net.Conn, error) {
    conn, err := ln.Listener.Accept()
    if err != nil {
        return nil, err
    }

    peekConn := newPeekConn(conn)

    b, err := peekConn.Peek(3)
    if err != nil {
        peekConn.Close()
        if err != io.EOF {
            return nil, err
        }
    }

    if b[0] == 0x16 && b[1] == 0x03 && b[2] <= 0x03 {
        log.Println("HTTPS")
        return tls.Server(peekConn, &tls.Config{
            ClientAuth: tls.NoClientCert,
            GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
                cert, err := tls.LoadX509KeyPair("server.cert.pem", "server.key.pem")
                if err != nil {
                    return nil, err
                }
                return &cert, nil
            },
        }), nil
    }

    log.Println("HTTP")
    return peekConn, nil
}

测试

我们使用 go run . 来启动服务,然后通过 cURL 来测试一下:

$ curl --resolve "*:6789:127.0.0.1" http://foreverz.cn:6789/123
hello from http!

$ curl --resolve "*:6789:127.0.0.1" https://foreverz.cn:6789/123
hello from https!

可以发现HTTP和HTTPS都能正常工作。

完整代码

所有代码都在GitHub上开源:

完结,撒花 🎉🎉🎉

参考

  1. Serving http and https on the same port?
  2. The Illustrated TLS 1.3 Connection
  3. Transport Layer Security (TLS) Parameters
  4. Nodejs HTTP and HTTPS over same port

Go

Part 1 of 1

Go 语言相关的文章

More from this blog

程序设计原则与代码设计模式

程序设计原则 SOLID 原则 1. 单一职责原则(SRP:Single Responsibility Principle) 一个类或模块应该只包含单一的职责,有且只有一个原因使其变更。 如果一个类或模块承担了过多的职责,那么它将变得难以维护和修改。例如,一个负责展示用户信息的组件不应该同时负责处理用户信息的逻辑,因为这两个职责是不同的。 2. 开闭原则(OCP:Open Closed Principle) 实体(类、模块、函数等)应该对扩展是开放的,对修改是封闭的。即可扩展(extension...

Jul 14, 202325 min read
程序设计原则与代码设计模式

ChatGPT 浅入浅出

ChatGPT 最近非常火爆,我最近看到了大量的关于 ChatGPT 的讨论和项目,这里就作为一个简单的教程和总结。 ChatGPT 是什么,能做什么? ChatGPT 本质上还是一个文本对话AI机器人,不过因为其知识库非常庞大(全网2021年前的公开内容),而且可以不断的更新自己的知识库,所以其能力非常的强大,而且其非常的聪明,再也不是“人工智障”了,与传统的人工智能相比,ChatGPT已经贴近人类的智能水平,基于其知识库的能力可以帮助我们做到很多人类做不到的事情,有些比较简单的重复性劳动也...

Mar 6, 20232 min read
ChatGPT 浅入浅出
U

Untitled Publication

12 posts