crypto/tls: split connErr to avoid read/write races.
Currently a write error will cause future reads to return that same error. However, there may have been extra information from a peer pending on the read direction that is now unavailable. This change splits the single connErr into errors for the read, write and handshake. (Splitting off the handshake error is needed because both read and write paths check the handshake error.) Fixes #7414. LGTM=bradfitz, r R=golang-codereviews, r, bradfitz CC=golang-codereviews https://golang.org/cl/69090044
This commit is contained in:
parent
8d65af24c4
commit
ef4934a9ed
127
conn.go
127
conn.go
@ -28,6 +28,7 @@ type Conn struct {
|
|||||||
|
|
||||||
// constant after handshake; protected by handshakeMutex
|
// constant after handshake; protected by handshakeMutex
|
||||||
handshakeMutex sync.Mutex // handshakeMutex < in.Mutex, out.Mutex, errMutex
|
handshakeMutex sync.Mutex // handshakeMutex < in.Mutex, out.Mutex, errMutex
|
||||||
|
handshakeErr error // error resulting from handshake
|
||||||
vers uint16 // TLS version
|
vers uint16 // TLS version
|
||||||
haveVers bool // version has been negotiated
|
haveVers bool // version has been negotiated
|
||||||
config *Config // configuration passed to constructor
|
config *Config // configuration passed to constructor
|
||||||
@ -45,9 +46,6 @@ type Conn struct {
|
|||||||
clientProtocol string
|
clientProtocol string
|
||||||
clientProtocolFallback bool
|
clientProtocolFallback bool
|
||||||
|
|
||||||
// first permanent error
|
|
||||||
connErr
|
|
||||||
|
|
||||||
// input/output
|
// input/output
|
||||||
in, out halfConn // in.Mutex < out.Mutex
|
in, out halfConn // in.Mutex < out.Mutex
|
||||||
rawInput *block // raw input, right off the wire
|
rawInput *block // raw input, right off the wire
|
||||||
@ -57,27 +55,6 @@ type Conn struct {
|
|||||||
tmp [16]byte
|
tmp [16]byte
|
||||||
}
|
}
|
||||||
|
|
||||||
type connErr struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
value error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *connErr) setError(err error) error {
|
|
||||||
e.mu.Lock()
|
|
||||||
defer e.mu.Unlock()
|
|
||||||
|
|
||||||
if e.value == nil {
|
|
||||||
e.value = err
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *connErr) error() error {
|
|
||||||
e.mu.Lock()
|
|
||||||
defer e.mu.Unlock()
|
|
||||||
return e.value
|
|
||||||
}
|
|
||||||
|
|
||||||
// Access to net.Conn methods.
|
// Access to net.Conn methods.
|
||||||
// Cannot just embed net.Conn because that would
|
// Cannot just embed net.Conn because that would
|
||||||
// export the struct field too.
|
// export the struct field too.
|
||||||
@ -116,6 +93,8 @@ func (c *Conn) SetWriteDeadline(t time.Time) error {
|
|||||||
// connection, either sending or receiving.
|
// connection, either sending or receiving.
|
||||||
type halfConn struct {
|
type halfConn struct {
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
|
|
||||||
|
err error // first permanent error
|
||||||
version uint16 // protocol version
|
version uint16 // protocol version
|
||||||
cipher interface{} // cipher algorithm
|
cipher interface{} // cipher algorithm
|
||||||
mac macFunction
|
mac macFunction
|
||||||
@ -129,6 +108,18 @@ type halfConn struct {
|
|||||||
inDigestBuf, outDigestBuf []byte
|
inDigestBuf, outDigestBuf []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (hc *halfConn) setErrorLocked(err error) error {
|
||||||
|
hc.err = err
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hc *halfConn) error() error {
|
||||||
|
hc.Lock()
|
||||||
|
err := hc.err
|
||||||
|
hc.Unlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// prepareCipherSpec sets the encryption and MAC states
|
// prepareCipherSpec sets the encryption and MAC states
|
||||||
// that a subsequent changeCipherSpec will use.
|
// that a subsequent changeCipherSpec will use.
|
||||||
func (hc *halfConn) prepareCipherSpec(version uint16, cipher interface{}, mac macFunction) {
|
func (hc *halfConn) prepareCipherSpec(version uint16, cipher interface{}, mac macFunction) {
|
||||||
@ -520,16 +511,16 @@ func (c *Conn) readRecord(want recordType) error {
|
|||||||
switch want {
|
switch want {
|
||||||
default:
|
default:
|
||||||
c.sendAlert(alertInternalError)
|
c.sendAlert(alertInternalError)
|
||||||
return errors.New("tls: unknown record type requested")
|
return c.in.setErrorLocked(errors.New("tls: unknown record type requested"))
|
||||||
case recordTypeHandshake, recordTypeChangeCipherSpec:
|
case recordTypeHandshake, recordTypeChangeCipherSpec:
|
||||||
if c.handshakeComplete {
|
if c.handshakeComplete {
|
||||||
c.sendAlert(alertInternalError)
|
c.sendAlert(alertInternalError)
|
||||||
return errors.New("tls: handshake or ChangeCipherSpec requested after handshake complete")
|
return c.in.setErrorLocked(errors.New("tls: handshake or ChangeCipherSpec requested after handshake complete"))
|
||||||
}
|
}
|
||||||
case recordTypeApplicationData:
|
case recordTypeApplicationData:
|
||||||
if !c.handshakeComplete {
|
if !c.handshakeComplete {
|
||||||
c.sendAlert(alertInternalError)
|
c.sendAlert(alertInternalError)
|
||||||
return errors.New("tls: application data record requested before handshake complete")
|
return c.in.setErrorLocked(errors.New("tls: application data record requested before handshake complete"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -548,7 +539,7 @@ Again:
|
|||||||
// err = io.ErrUnexpectedEOF
|
// err = io.ErrUnexpectedEOF
|
||||||
// }
|
// }
|
||||||
if e, ok := err.(net.Error); !ok || !e.Temporary() {
|
if e, ok := err.(net.Error); !ok || !e.Temporary() {
|
||||||
c.setError(err)
|
c.in.setErrorLocked(err)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -560,18 +551,18 @@ Again:
|
|||||||
// an SSLv2 client.
|
// an SSLv2 client.
|
||||||
if want == recordTypeHandshake && typ == 0x80 {
|
if want == recordTypeHandshake && typ == 0x80 {
|
||||||
c.sendAlert(alertProtocolVersion)
|
c.sendAlert(alertProtocolVersion)
|
||||||
return errors.New("tls: unsupported SSLv2 handshake received")
|
return c.in.setErrorLocked(errors.New("tls: unsupported SSLv2 handshake received"))
|
||||||
}
|
}
|
||||||
|
|
||||||
vers := uint16(b.data[1])<<8 | uint16(b.data[2])
|
vers := uint16(b.data[1])<<8 | uint16(b.data[2])
|
||||||
n := int(b.data[3])<<8 | int(b.data[4])
|
n := int(b.data[3])<<8 | int(b.data[4])
|
||||||
if c.haveVers && vers != c.vers {
|
if c.haveVers && vers != c.vers {
|
||||||
c.sendAlert(alertProtocolVersion)
|
c.sendAlert(alertProtocolVersion)
|
||||||
return fmt.Errorf("tls: received record with version %x when expecting version %x", vers, c.vers)
|
return c.in.setErrorLocked(fmt.Errorf("tls: received record with version %x when expecting version %x", vers, c.vers))
|
||||||
}
|
}
|
||||||
if n > maxCiphertext {
|
if n > maxCiphertext {
|
||||||
c.sendAlert(alertRecordOverflow)
|
c.sendAlert(alertRecordOverflow)
|
||||||
return fmt.Errorf("tls: oversized record received with length %d", n)
|
return c.in.setErrorLocked(fmt.Errorf("tls: oversized record received with length %d", n))
|
||||||
}
|
}
|
||||||
if !c.haveVers {
|
if !c.haveVers {
|
||||||
// First message, be extra suspicious:
|
// First message, be extra suspicious:
|
||||||
@ -584,7 +575,7 @@ Again:
|
|||||||
// it's probably not real.
|
// it's probably not real.
|
||||||
if (typ != recordTypeAlert && typ != want) || vers >= 0x1000 || n >= 0x3000 {
|
if (typ != recordTypeAlert && typ != want) || vers >= 0x1000 || n >= 0x3000 {
|
||||||
c.sendAlert(alertUnexpectedMessage)
|
c.sendAlert(alertUnexpectedMessage)
|
||||||
return fmt.Errorf("tls: first record does not look like a TLS handshake")
|
return c.in.setErrorLocked(fmt.Errorf("tls: first record does not look like a TLS handshake"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := b.readFromUntil(c.conn, recordHeaderLen+n); err != nil {
|
if err := b.readFromUntil(c.conn, recordHeaderLen+n); err != nil {
|
||||||
@ -592,7 +583,7 @@ Again:
|
|||||||
err = io.ErrUnexpectedEOF
|
err = io.ErrUnexpectedEOF
|
||||||
}
|
}
|
||||||
if e, ok := err.(net.Error); !ok || !e.Temporary() {
|
if e, ok := err.(net.Error); !ok || !e.Temporary() {
|
||||||
c.setError(err)
|
c.in.setErrorLocked(err)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -601,27 +592,27 @@ Again:
|
|||||||
b, c.rawInput = c.in.splitBlock(b, recordHeaderLen+n)
|
b, c.rawInput = c.in.splitBlock(b, recordHeaderLen+n)
|
||||||
ok, off, err := c.in.decrypt(b)
|
ok, off, err := c.in.decrypt(b)
|
||||||
if !ok {
|
if !ok {
|
||||||
return c.sendAlert(err)
|
c.in.setErrorLocked(c.sendAlert(err))
|
||||||
}
|
}
|
||||||
b.off = off
|
b.off = off
|
||||||
data := b.data[b.off:]
|
data := b.data[b.off:]
|
||||||
if len(data) > maxPlaintext {
|
if len(data) > maxPlaintext {
|
||||||
c.sendAlert(alertRecordOverflow)
|
err := c.sendAlert(alertRecordOverflow)
|
||||||
c.in.freeBlock(b)
|
c.in.freeBlock(b)
|
||||||
return c.error()
|
return c.in.setErrorLocked(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch typ {
|
switch typ {
|
||||||
default:
|
default:
|
||||||
c.sendAlert(alertUnexpectedMessage)
|
c.in.setErrorLocked(c.sendAlert(alertUnexpectedMessage))
|
||||||
|
|
||||||
case recordTypeAlert:
|
case recordTypeAlert:
|
||||||
if len(data) != 2 {
|
if len(data) != 2 {
|
||||||
c.sendAlert(alertUnexpectedMessage)
|
c.in.setErrorLocked(c.sendAlert(alertUnexpectedMessage))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if alert(data[1]) == alertCloseNotify {
|
if alert(data[1]) == alertCloseNotify {
|
||||||
c.setError(io.EOF)
|
c.in.setErrorLocked(io.EOF)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
switch data[0] {
|
switch data[0] {
|
||||||
@ -630,24 +621,24 @@ Again:
|
|||||||
c.in.freeBlock(b)
|
c.in.freeBlock(b)
|
||||||
goto Again
|
goto Again
|
||||||
case alertLevelError:
|
case alertLevelError:
|
||||||
c.setError(&net.OpError{Op: "remote error", Err: alert(data[1])})
|
c.in.setErrorLocked(&net.OpError{Op: "remote error", Err: alert(data[1])})
|
||||||
default:
|
default:
|
||||||
c.sendAlert(alertUnexpectedMessage)
|
c.in.setErrorLocked(c.sendAlert(alertUnexpectedMessage))
|
||||||
}
|
}
|
||||||
|
|
||||||
case recordTypeChangeCipherSpec:
|
case recordTypeChangeCipherSpec:
|
||||||
if typ != want || len(data) != 1 || data[0] != 1 {
|
if typ != want || len(data) != 1 || data[0] != 1 {
|
||||||
c.sendAlert(alertUnexpectedMessage)
|
c.in.setErrorLocked(c.sendAlert(alertUnexpectedMessage))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
err := c.in.changeCipherSpec()
|
err := c.in.changeCipherSpec()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.sendAlert(err.(alert))
|
c.in.setErrorLocked(c.sendAlert(err.(alert)))
|
||||||
}
|
}
|
||||||
|
|
||||||
case recordTypeApplicationData:
|
case recordTypeApplicationData:
|
||||||
if typ != want {
|
if typ != want {
|
||||||
c.sendAlert(alertUnexpectedMessage)
|
c.in.setErrorLocked(c.sendAlert(alertUnexpectedMessage))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
c.input = b
|
c.input = b
|
||||||
@ -656,7 +647,7 @@ Again:
|
|||||||
case recordTypeHandshake:
|
case recordTypeHandshake:
|
||||||
// TODO(rsc): Should at least pick off connection close.
|
// TODO(rsc): Should at least pick off connection close.
|
||||||
if typ != want {
|
if typ != want {
|
||||||
return c.sendAlert(alertNoRenegotiation)
|
return c.in.setErrorLocked(c.sendAlert(alertNoRenegotiation))
|
||||||
}
|
}
|
||||||
c.hand.Write(data)
|
c.hand.Write(data)
|
||||||
}
|
}
|
||||||
@ -664,7 +655,7 @@ Again:
|
|||||||
if b != nil {
|
if b != nil {
|
||||||
c.in.freeBlock(b)
|
c.in.freeBlock(b)
|
||||||
}
|
}
|
||||||
return c.error()
|
return c.in.err
|
||||||
}
|
}
|
||||||
|
|
||||||
// sendAlert sends a TLS alert message.
|
// sendAlert sends a TLS alert message.
|
||||||
@ -680,7 +671,7 @@ func (c *Conn) sendAlertLocked(err alert) error {
|
|||||||
c.writeRecord(recordTypeAlert, c.tmp[0:2])
|
c.writeRecord(recordTypeAlert, c.tmp[0:2])
|
||||||
// closeNotify is a special case in that it isn't an error:
|
// closeNotify is a special case in that it isn't an error:
|
||||||
if err != alertCloseNotify {
|
if err != alertCloseNotify {
|
||||||
return c.setError(&net.OpError{Op: "local error", Err: err})
|
return c.out.setErrorLocked(&net.OpError{Op: "local error", Err: err})
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -766,7 +757,7 @@ func (c *Conn) writeRecord(typ recordType, data []byte) (n int, err error) {
|
|||||||
c.tmp[0] = alertLevelError
|
c.tmp[0] = alertLevelError
|
||||||
c.tmp[1] = byte(err.(alert))
|
c.tmp[1] = byte(err.(alert))
|
||||||
c.writeRecord(recordTypeAlert, c.tmp[0:2])
|
c.writeRecord(recordTypeAlert, c.tmp[0:2])
|
||||||
return n, c.setError(&net.OpError{Op: "local error", Err: err})
|
return n, c.out.setErrorLocked(&net.OpError{Op: "local error", Err: err})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@ -777,7 +768,7 @@ func (c *Conn) writeRecord(typ recordType, data []byte) (n int, err error) {
|
|||||||
// c.in.Mutex < L; c.out.Mutex < L.
|
// c.in.Mutex < L; c.out.Mutex < L.
|
||||||
func (c *Conn) readHandshake() (interface{}, error) {
|
func (c *Conn) readHandshake() (interface{}, error) {
|
||||||
for c.hand.Len() < 4 {
|
for c.hand.Len() < 4 {
|
||||||
if err := c.error(); err != nil {
|
if err := c.in.err; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := c.readRecord(recordTypeHandshake); err != nil {
|
if err := c.readRecord(recordTypeHandshake); err != nil {
|
||||||
@ -788,11 +779,10 @@ func (c *Conn) readHandshake() (interface{}, error) {
|
|||||||
data := c.hand.Bytes()
|
data := c.hand.Bytes()
|
||||||
n := int(data[1])<<16 | int(data[2])<<8 | int(data[3])
|
n := int(data[1])<<16 | int(data[2])<<8 | int(data[3])
|
||||||
if n > maxHandshake {
|
if n > maxHandshake {
|
||||||
c.sendAlert(alertInternalError)
|
return nil, c.in.setErrorLocked(c.sendAlert(alertInternalError))
|
||||||
return nil, c.error()
|
|
||||||
}
|
}
|
||||||
for c.hand.Len() < 4+n {
|
for c.hand.Len() < 4+n {
|
||||||
if err := c.error(); err != nil {
|
if err := c.in.err; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := c.readRecord(recordTypeHandshake); err != nil {
|
if err := c.readRecord(recordTypeHandshake); err != nil {
|
||||||
@ -831,8 +821,7 @@ func (c *Conn) readHandshake() (interface{}, error) {
|
|||||||
case typeFinished:
|
case typeFinished:
|
||||||
m = new(finishedMsg)
|
m = new(finishedMsg)
|
||||||
default:
|
default:
|
||||||
c.sendAlert(alertUnexpectedMessage)
|
return nil, c.in.setErrorLocked(c.sendAlert(alertUnexpectedMessage))
|
||||||
return nil, alertUnexpectedMessage
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The handshake message unmarshallers
|
// The handshake message unmarshallers
|
||||||
@ -841,25 +830,24 @@ func (c *Conn) readHandshake() (interface{}, error) {
|
|||||||
data = append([]byte(nil), data...)
|
data = append([]byte(nil), data...)
|
||||||
|
|
||||||
if !m.unmarshal(data) {
|
if !m.unmarshal(data) {
|
||||||
c.sendAlert(alertUnexpectedMessage)
|
return nil, c.in.setErrorLocked(c.sendAlert(alertUnexpectedMessage))
|
||||||
return nil, alertUnexpectedMessage
|
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write writes data to the connection.
|
// Write writes data to the connection.
|
||||||
func (c *Conn) Write(b []byte) (int, error) {
|
func (c *Conn) Write(b []byte) (int, error) {
|
||||||
if err := c.error(); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.Handshake(); err != nil {
|
if err := c.Handshake(); err != nil {
|
||||||
return 0, c.setError(err)
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
c.out.Lock()
|
c.out.Lock()
|
||||||
defer c.out.Unlock()
|
defer c.out.Unlock()
|
||||||
|
|
||||||
|
if err := c.out.err; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
if !c.handshakeComplete {
|
if !c.handshakeComplete {
|
||||||
return 0, alertInternalError
|
return 0, alertInternalError
|
||||||
}
|
}
|
||||||
@ -878,14 +866,14 @@ func (c *Conn) Write(b []byte) (int, error) {
|
|||||||
if _, ok := c.out.cipher.(cipher.BlockMode); ok {
|
if _, ok := c.out.cipher.(cipher.BlockMode); ok {
|
||||||
n, err := c.writeRecord(recordTypeApplicationData, b[:1])
|
n, err := c.writeRecord(recordTypeApplicationData, b[:1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return n, c.setError(err)
|
return n, c.out.setErrorLocked(err)
|
||||||
}
|
}
|
||||||
m, b = 1, b[1:]
|
m, b = 1, b[1:]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
n, err := c.writeRecord(recordTypeApplicationData, b)
|
n, err := c.writeRecord(recordTypeApplicationData, b)
|
||||||
return n + m, c.setError(err)
|
return n + m, c.out.setErrorLocked(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read can be made to time out and return a net.Error with Timeout() == true
|
// Read can be made to time out and return a net.Error with Timeout() == true
|
||||||
@ -902,13 +890,13 @@ func (c *Conn) Read(b []byte) (n int, err error) {
|
|||||||
// CBC IV. So this loop ignores a limited number of empty records.
|
// CBC IV. So this loop ignores a limited number of empty records.
|
||||||
const maxConsecutiveEmptyRecords = 100
|
const maxConsecutiveEmptyRecords = 100
|
||||||
for emptyRecordCount := 0; emptyRecordCount <= maxConsecutiveEmptyRecords; emptyRecordCount++ {
|
for emptyRecordCount := 0; emptyRecordCount <= maxConsecutiveEmptyRecords; emptyRecordCount++ {
|
||||||
for c.input == nil && c.error() == nil {
|
for c.input == nil && c.in.err == nil {
|
||||||
if err := c.readRecord(recordTypeApplicationData); err != nil {
|
if err := c.readRecord(recordTypeApplicationData); err != nil {
|
||||||
// Soft error, like EAGAIN
|
// Soft error, like EAGAIN
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := c.error(); err != nil {
|
if err := c.in.err; err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -949,16 +937,19 @@ func (c *Conn) Close() error {
|
|||||||
func (c *Conn) Handshake() error {
|
func (c *Conn) Handshake() error {
|
||||||
c.handshakeMutex.Lock()
|
c.handshakeMutex.Lock()
|
||||||
defer c.handshakeMutex.Unlock()
|
defer c.handshakeMutex.Unlock()
|
||||||
if err := c.error(); err != nil {
|
if err := c.handshakeErr; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if c.handshakeComplete {
|
if c.handshakeComplete {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.isClient {
|
if c.isClient {
|
||||||
return c.clientHandshake()
|
c.handshakeErr = c.clientHandshake()
|
||||||
|
} else {
|
||||||
|
c.handshakeErr = c.serverHandshake()
|
||||||
}
|
}
|
||||||
return c.serverHandshake()
|
return c.handshakeErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConnectionState returns basic TLS details about the connection.
|
// ConnectionState returns basic TLS details about the connection.
|
||||||
|
@ -501,7 +501,7 @@ func (hs *clientHandshakeState) readFinished() error {
|
|||||||
c := hs.c
|
c := hs.c
|
||||||
|
|
||||||
c.readRecord(recordTypeChangeCipherSpec)
|
c.readRecord(recordTypeChangeCipherSpec)
|
||||||
if err := c.error(); err != nil {
|
if err := c.in.error(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -470,7 +470,7 @@ func (hs *serverHandshakeState) readFinished() error {
|
|||||||
c := hs.c
|
c := hs.c
|
||||||
|
|
||||||
c.readRecord(recordTypeChangeCipherSpec)
|
c.readRecord(recordTypeChangeCipherSpec)
|
||||||
if err := c.error(); err != nil {
|
if err := c.in.error(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user