mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-23 22:27:38 -05:00
mitm: Add experimental Tor support for interception detection
This commit is contained in:
parent
8051c73cc3
commit
0da76e2b76
3 changed files with 142 additions and 10 deletions
|
@ -65,7 +65,16 @@ func (h *tlsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
mitm = !info.looksLikeChrome() && !info.looksLikeSafari()
|
mitm = !info.looksLikeChrome() && !info.looksLikeSafari()
|
||||||
} else if strings.Contains(ua, "Firefox") {
|
} else if strings.Contains(ua, "Firefox") {
|
||||||
checked = true
|
checked = true
|
||||||
mitm = !info.looksLikeFirefox()
|
if strings.Contains(ua, "Windows") {
|
||||||
|
ver := getVersion(ua, "Firefox")
|
||||||
|
if ver == 45.0 || ver == 52.0 {
|
||||||
|
mitm = !info.looksLikeTor()
|
||||||
|
} else {
|
||||||
|
mitm = !info.looksLikeFirefox()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mitm = !info.looksLikeFirefox()
|
||||||
|
}
|
||||||
} else if strings.Contains(ua, "Safari") {
|
} else if strings.Contains(ua, "Safari") {
|
||||||
checked = true
|
checked = true
|
||||||
mitm = !info.looksLikeSafari()
|
mitm = !info.looksLikeSafari()
|
||||||
|
@ -87,6 +96,34 @@ func (h *tlsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
h.next.ServeHTTP(w, r)
|
h.next.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getVersion returns a (possibly simplified) representation of the version string
|
||||||
|
// from a UserAgent string. It returns a float, so it can represent major and minor
|
||||||
|
// versions; the rest of the version is just tacked on behind the decimal point.
|
||||||
|
// The purpose of this is to stay simple while allowing for basic, fast comparisons.
|
||||||
|
// If the version for softwareName is not found in ua, -1 is returned.
|
||||||
|
func getVersion(ua, softwareName string) float64 {
|
||||||
|
search := softwareName + "/"
|
||||||
|
start := strings.Index(ua, search)
|
||||||
|
if start < 0 {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
start += len(search)
|
||||||
|
end := strings.Index(ua[start:], " ")
|
||||||
|
if end < 0 {
|
||||||
|
end = len(ua)
|
||||||
|
}
|
||||||
|
strVer := strings.Replace(ua[start:end], "-", "", -1)
|
||||||
|
firstDot := strings.Index(strVer, ".")
|
||||||
|
if firstDot >= 0 {
|
||||||
|
strVer = strVer[:firstDot+1] + strings.Replace(strVer[firstDot+1:], ".", "", -1)
|
||||||
|
}
|
||||||
|
ver, err := strconv.ParseFloat(strVer, 64)
|
||||||
|
if err != nil {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return ver
|
||||||
|
}
|
||||||
|
|
||||||
// clientHelloConn reads the ClientHello
|
// clientHelloConn reads the ClientHello
|
||||||
// and stores it in the attached listener.
|
// and stores it in the attached listener.
|
||||||
type clientHelloConn struct {
|
type clientHelloConn struct {
|
||||||
|
@ -330,6 +367,7 @@ func (info rawHelloInfo) looksLikeFirefox() bool {
|
||||||
// Note: Firefox 51+ does not advertise 0x3374 (13172, NPN).
|
// Note: Firefox 51+ does not advertise 0x3374 (13172, NPN).
|
||||||
// Note: Firefox doesn't advertise 0x0 (0, SNI) when connecting to IP addresses.
|
// Note: Firefox doesn't advertise 0x0 (0, SNI) when connecting to IP addresses.
|
||||||
// Note: Firefox 55+ doesn't appear to advertise 0xFF03 (65283, short headers). It used to be between 5 and 13.
|
// Note: Firefox 55+ doesn't appear to advertise 0xFF03 (65283, short headers). It used to be between 5 and 13.
|
||||||
|
// Note: Firefox on Fedora (or RedHat) doesn't include ECC suites because of patent liability.
|
||||||
requiredExtensionsOrder := []uint16{23, 65281, 10, 11, 35, 16, 5, 13}
|
requiredExtensionsOrder := []uint16{23, 65281, 10, 11, 35, 16, 5, 13}
|
||||||
if !assertPresenceAndOrdering(requiredExtensionsOrder, info.extensions, true) {
|
if !assertPresenceAndOrdering(requiredExtensionsOrder, info.extensions, true) {
|
||||||
return false
|
return false
|
||||||
|
@ -543,6 +581,70 @@ func (info rawHelloInfo) looksLikeSafari() bool {
|
||||||
return assertPresenceAndOrdering(expectedCipherSuiteOrder, info.cipherSuites, true)
|
return assertPresenceAndOrdering(expectedCipherSuiteOrder, info.cipherSuites, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// looksLikeTor returns true if the info looks like a ClientHello from Tor browser
|
||||||
|
// (based on Firefox).
|
||||||
|
func (info rawHelloInfo) looksLikeTor() bool {
|
||||||
|
requiredExtensionsOrder := []uint16{10, 11, 16, 5, 13}
|
||||||
|
if !assertPresenceAndOrdering(requiredExtensionsOrder, info.extensions, true) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for session tickets support; Tor doesn't support them to prevent tracking
|
||||||
|
for _, ext := range info.extensions {
|
||||||
|
if ext == 35 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We check for both presence of curves and their ordering, including
|
||||||
|
// an optional curve at the beginning (for Tor based on Firefox 52)
|
||||||
|
infoCurves := info.curves
|
||||||
|
if len(info.curves) == 4 {
|
||||||
|
if info.curves[0] != 29 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
infoCurves = info.curves[1:]
|
||||||
|
}
|
||||||
|
requiredCurves := []tls.CurveID{23, 24, 25}
|
||||||
|
if len(infoCurves) < len(requiredCurves) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := range requiredCurves {
|
||||||
|
if infoCurves[i] != requiredCurves[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasGreaseCiphers(info.cipherSuites) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// We check for order of cipher suites but not presence, since
|
||||||
|
// according to the paper, cipher suites may be not be added
|
||||||
|
// or reordered by the user, but they may be disabled.
|
||||||
|
expectedCipherSuiteOrder := []uint16{
|
||||||
|
TLS_AES_128_GCM_SHA256, // 0x1301
|
||||||
|
TLS_CHACHA20_POLY1305_SHA256, // 0x1303
|
||||||
|
TLS_AES_256_GCM_SHA384, // 0x1302
|
||||||
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, // 0xc02b
|
||||||
|
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, // 0xc02f
|
||||||
|
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, // 0xcca9
|
||||||
|
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, // 0xcca8
|
||||||
|
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, // 0xc02c
|
||||||
|
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, // 0xc030
|
||||||
|
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, // 0xc00a
|
||||||
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, // 0xc009
|
||||||
|
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, // 0xc013
|
||||||
|
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, // 0xc014
|
||||||
|
TLS_DHE_RSA_WITH_AES_128_CBC_SHA, // 0x33
|
||||||
|
TLS_DHE_RSA_WITH_AES_256_CBC_SHA, // 0x39
|
||||||
|
tls.TLS_RSA_WITH_AES_128_CBC_SHA, // 0x2f
|
||||||
|
tls.TLS_RSA_WITH_AES_256_CBC_SHA, // 0x35
|
||||||
|
tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, // 0xa
|
||||||
|
}
|
||||||
|
return assertPresenceAndOrdering(expectedCipherSuiteOrder, info.cipherSuites, false)
|
||||||
|
}
|
||||||
|
|
||||||
// assertPresenceAndOrdering will return true if candidateList contains
|
// assertPresenceAndOrdering will return true if candidateList contains
|
||||||
// the items in requiredItems in the same order as requiredItems.
|
// the items in requiredItems in the same order as requiredItems.
|
||||||
//
|
//
|
||||||
|
|
|
@ -132,6 +132,16 @@ func TestHeuristicFunctionsAndHandler(t *testing.T) {
|
||||||
helloHex: `010001fc030383141d213d1bf069171843489faf808028d282c9828e1ba87637c863833c730720a67e76e152f4b704523b72317ef4587e231f02e2395e0ecac6be9f28c35e6ce600208a8ac02bc02fc02cc030cca9cca8cc14cc13c013c014009c009d002f0035000a010001931a1a0000ff0100010000000014001200000f66696e6572706978656c732e636f6d00170000002300785e85429bf1764f33111cd3ad5d1c56d765976fd962b49dbecbb6f7865e2a8d8536ad854f1fa99a8bbbf998814fee54a63a0bf162869d2bba37e9778304e7c4140825718e191b574c6246a0611de6447bdd80417f83ff9d9b7124069a9f74b90394ecb89bec5f6a1a67c1b89e50b8674782f53dd51807651a000d00140012040308040401050308050501080606010201000500050100000000001200000010000e000c02683208687474702f312e3175500000000b00020100000a000a00081a1a001d001700182a2a0001000015009a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000`,
|
helloHex: `010001fc030383141d213d1bf069171843489faf808028d282c9828e1ba87637c863833c730720a67e76e152f4b704523b72317ef4587e231f02e2395e0ecac6be9f28c35e6ce600208a8ac02bc02fc02cc030cca9cca8cc14cc13c013c014009c009d002f0035000a010001931a1a0000ff0100010000000014001200000f66696e6572706978656c732e636f6d00170000002300785e85429bf1764f33111cd3ad5d1c56d765976fd962b49dbecbb6f7865e2a8d8536ad854f1fa99a8bbbf998814fee54a63a0bf162869d2bba37e9778304e7c4140825718e191b574c6246a0611de6447bdd80417f83ff9d9b7124069a9f74b90394ecb89bec5f6a1a67c1b89e50b8674782f53dd51807651a000d00140012040308040401050308050501080606010201000500050100000000001200000010000e000c02683208687474702f312e3175500000000b00020100000a000a00081a1a001d001700182a2a0001000015009a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000`,
|
||||||
interception: false,
|
interception: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36",
|
||||||
|
helloHex: `010000c203034166c97e2016046e0c88ad867c410d0aee470f4d9b4ec8fe41a751d2a6348e3100001c4a4ac02bc02fc02cc030cca9cca8c013c014009c009d002f0035000a0100007dcaca0000ff0100010000000014001200000f66696e6572706978656c732e636f6d0017000000230000000d00140012040308040401050308050501080606010201000500050100000000001200000010000e000c02683208687474702f312e3175500000000b00020100000a000a00086a6a001d001700187a7a000100`,
|
||||||
|
interception: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36",
|
||||||
|
helloHex: `010000c203037741795e73cd5b4949f79a0dc9cccc8b006e4c0ec324f965c6fe9f0833909f0100001c7a7ac02bc02fc02cc030cca9cca8c013c014009c009d002f0035000a0100007d7a7a0000ff0100010000000014001200000f66696e6572706978656c732e636f6d0017000000230000000d00140012040308040401050308050501080606010201000500050100000000001200000010000e000c02683208687474702f312e3175500000000b00020100000a000a00084a4a001d001700185a5a000100`,
|
||||||
|
interception: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"Firefox": {
|
"Firefox": {
|
||||||
{
|
{
|
||||||
|
@ -155,6 +165,12 @@ func TestHeuristicFunctionsAndHandler(t *testing.T) {
|
||||||
helloHex: `010001fc030331e380b7d12018e1202ef3327607203df5c5732b4fa5ab5abaf0b60034c2fb662070c836b9b89123e37f4f1074d152df438fa8ee8a0f89b036fd952f4fcc0b994f001c130113031302c02bc02fcca9cca8c02cc030c013c014002f0035000a0100019700000014001200000f63616464797365727665722e636f6d00170000ff01000100000a000e000c001d00170018001901000101000b0002010000230078c97e7716a041e2ea824571bef26a3dff2bf50a883cd15d904ab2d17deb514f6e0a079ee7c212c000178387ffafc2e530b6df6662f570aae134330f13c458a0eaad5a96a9696f572110918740b15db1143d19aaaa706942030b433a7e6150f62b443c0564e5b8f7ee9577bf3bf7faec8c67425b648ab54d880010000e000c02683208687474702f312e310005000501000000000028006b0069001d0020aee6e596155ee6f79f943e81ceabe0979d27fbbb8b9189ccb2ebc75226351f32001700410421875a44e510decac11ef1d7cfddd4dfe105d5cd3a2d42fba03ebde23e51e8ce65bda1b48be82d4848d1db2bfce68e94092e925a9ce0dbf5df35479558108489002b0009087f12030303020301000d0018001604030503060308040805080604010501060102030201002d000201010015002500000000000000000000000000000000000000000000000000000000000000000000000000`,
|
helloHex: `010001fc030331e380b7d12018e1202ef3327607203df5c5732b4fa5ab5abaf0b60034c2fb662070c836b9b89123e37f4f1074d152df438fa8ee8a0f89b036fd952f4fcc0b994f001c130113031302c02bc02fcca9cca8c02cc030c013c014002f0035000a0100019700000014001200000f63616464797365727665722e636f6d00170000ff01000100000a000e000c001d00170018001901000101000b0002010000230078c97e7716a041e2ea824571bef26a3dff2bf50a883cd15d904ab2d17deb514f6e0a079ee7c212c000178387ffafc2e530b6df6662f570aae134330f13c458a0eaad5a96a9696f572110918740b15db1143d19aaaa706942030b433a7e6150f62b443c0564e5b8f7ee9577bf3bf7faec8c67425b648ab54d880010000e000c02683208687474702f312e310005000501000000000028006b0069001d0020aee6e596155ee6f79f943e81ceabe0979d27fbbb8b9189ccb2ebc75226351f32001700410421875a44e510decac11ef1d7cfddd4dfe105d5cd3a2d42fba03ebde23e51e8ce65bda1b48be82d4848d1db2bfce68e94092e925a9ce0dbf5df35479558108489002b0009087f12030303020301000d0018001604030503060308040805080604010501060102030201002d000201010015002500000000000000000000000000000000000000000000000000000000000000000000000000`,
|
||||||
interception: false,
|
interception: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Firefox on Fedora (RedHat) doesn't include ECC ciphers because of patent liabilities
|
||||||
|
userAgent: "Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:53.0) Gecko/20100101 Firefox/53.0",
|
||||||
|
helloHex: `010000b70303f5280b74d617d42e39fd77b78a2b537b1d7787ce4fcbcf3604c9fbcd677c6c5500001ec02bc02fcca9cca8c02cc030c00ac009c013c01400330039002f0035000a0100007000000014001200000f66696e6572706978656c732e636f6d00170000ff01000100000a000a0008001d001700180019000b00020100002300000010000e000c02683208687474702f312e31000500050100000000000d0018001604030503060308040805080604010501060102030201`,
|
||||||
|
interception: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"Edge": {
|
"Edge": {
|
||||||
{
|
{
|
||||||
|
@ -170,6 +186,18 @@ func TestHeuristicFunctionsAndHandler(t *testing.T) {
|
||||||
interception: false,
|
interception: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"Tor": {
|
||||||
|
{
|
||||||
|
userAgent: "Mozilla/5.0 (Windows NT 6.1; rv:45.0) Gecko/20100101 Firefox/45.0",
|
||||||
|
helloHex: `010000a40303137f05d4151f2d9095aee4254416d9dce73d6a1d857e8097ea20d021c04a7a81000016c02bc02fc00ac009c013c01400330039002f0035000a0100006500000014001200000f66696e6572706978656c732e636f6dff01000100000a00080006001700180019000b00020100337400000010000b000908687474702f312e31000500050100000000000d001600140401050106010201040305030603020304020202`,
|
||||||
|
interception: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userAgent: "Mozilla/5.0 (Windows NT 6.1; rv:52.0) Gecko/20100101 Firefox/52.0",
|
||||||
|
helloHex: `010000b4030322e1f3aff4c37caba303c2ce53ba1689b3e70117a46f413d44f70a74cb6a496100001ec02bc02fcca9cca8c02cc030c00ac009c013c01400330039002f0035000a0100006d00000014001200000f66696e6572706978656c732e636f6d00170000ff01000100000a000a0008001d001700180019000b000201000010000b000908687474702f312e31000500050100000000ff030000000d0018001604030503060308040805080604010501060102030201`,
|
||||||
|
interception: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
"Other": { // these are either non-browser clients or intercepted client hellos
|
"Other": { // these are either non-browser clients or intercepted client hellos
|
||||||
{
|
{
|
||||||
// openssl s_client (OpenSSL 0.9.8zh 14 Jan 2016) - NOT an interception, but not a browser either
|
// openssl s_client (OpenSSL 0.9.8zh 14 Jan 2016) - NOT an interception, but not a browser either
|
||||||
|
@ -237,7 +265,7 @@ func TestHeuristicFunctionsAndHandler(t *testing.T) {
|
||||||
interception: true,
|
interception: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// IE 11 on Windows 10, intercepted by Fortigate (same firewallas above)
|
// IE 11 on Windows 10, intercepted by Fortigate (same firewall as above)
|
||||||
userAgent: "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko",
|
userAgent: "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko",
|
||||||
helloHex: `010000e5030158ac634c5278d7b17421f23a64cc91d68c470c6b247322fe867ba035b373d05c000064003300320039003800160013c013c009c014c00ac012c008002f0035000a00150012003d003c00670040006b006ac011c0070096009a009900410084004500440088008700ba00be00bd00c000c400c3c03cc044c042c03dc045c04300090005000400ff01000058000a003600340000000100020003000400050006000700080009000a000b000c000d000e000f0010001100120013001400150016001700180019000b0002010000000014001200000f66696e6572706978656c732e636f6d`,
|
helloHex: `010000e5030158ac634c5278d7b17421f23a64cc91d68c470c6b247322fe867ba035b373d05c000064003300320039003800160013c013c009c014c00ac012c008002f0035000a00150012003d003c00670040006b006ac011c0070096009a009900410084004500440088008700ba00be00bd00c000c400c3c03cc044c042c03dc045c04300090005000400ff01000058000a003600340000000100020003000400050006000700080009000a000b000c000d000e000f0010001100120013001400150016001700180019000b0002010000000014001200000f66696e6572706978656c732e636f6d`,
|
||||||
interception: true,
|
interception: true,
|
||||||
|
@ -270,6 +298,7 @@ func TestHeuristicFunctionsAndHandler(t *testing.T) {
|
||||||
isFirefox := parsed.looksLikeFirefox()
|
isFirefox := parsed.looksLikeFirefox()
|
||||||
isSafari := parsed.looksLikeSafari()
|
isSafari := parsed.looksLikeSafari()
|
||||||
isEdge := parsed.looksLikeEdge()
|
isEdge := parsed.looksLikeEdge()
|
||||||
|
isTor := parsed.looksLikeTor()
|
||||||
|
|
||||||
// we want each of the heuristic functions to be as
|
// we want each of the heuristic functions to be as
|
||||||
// exclusive but as low-maintenance as possible;
|
// exclusive but as low-maintenance as possible;
|
||||||
|
@ -280,20 +309,22 @@ func TestHeuristicFunctionsAndHandler(t *testing.T) {
|
||||||
var correct bool
|
var correct bool
|
||||||
switch client {
|
switch client {
|
||||||
case "Chrome":
|
case "Chrome":
|
||||||
correct = isChrome && !isFirefox && !isSafari && !isEdge
|
correct = isChrome && !isFirefox && !isSafari && !isEdge && !isTor
|
||||||
case "Firefox":
|
case "Firefox":
|
||||||
correct = !isChrome && isFirefox && !isSafari && !isEdge
|
correct = !isChrome && isFirefox && !isSafari && !isEdge && !isTor
|
||||||
case "Safari":
|
case "Safari":
|
||||||
correct = !isChrome && !isFirefox && isSafari && !isEdge
|
correct = !isChrome && !isFirefox && isSafari && !isEdge && !isTor
|
||||||
case "Edge":
|
case "Edge":
|
||||||
correct = !isChrome && !isFirefox && !isSafari && isEdge
|
correct = !isChrome && !isFirefox && !isSafari && isEdge && !isTor
|
||||||
|
case "Tor":
|
||||||
|
correct = !isChrome && !isFirefox && !isSafari && !isEdge && isTor
|
||||||
case "Other":
|
case "Other":
|
||||||
correct = !isChrome && !isFirefox && !isSafari && !isEdge
|
correct = !isChrome && !isFirefox && !isSafari && !isEdge && !isTor
|
||||||
}
|
}
|
||||||
|
|
||||||
if !correct {
|
if !correct {
|
||||||
t.Errorf("[%s] Test %d: Chrome=%v, Firefox=%v, Safari=%v, Edge=%v; parsed hello: %+v",
|
t.Errorf("[%s] Test %d: Chrome=%v Firefox=%v Safari=%v Edge=%v Tor=%v\n\tparsed hello dec: %+v\n\tparsed hello hex: %#x\n",
|
||||||
client, i, isChrome, isFirefox, isSafari, isEdge, parsed)
|
client, i, isChrome, isFirefox, isSafari, isEdge, isTor, parsed, parsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
// test the handler too
|
// test the handler too
|
||||||
|
|
|
@ -312,7 +312,6 @@ func (r *replacer) getSubstitution(key string) string {
|
||||||
if val {
|
if val {
|
||||||
return "likely"
|
return "likely"
|
||||||
}
|
}
|
||||||
|
|
||||||
return "unlikely"
|
return "unlikely"
|
||||||
}
|
}
|
||||||
return "unknown"
|
return "unknown"
|
||||||
|
|
Loading…
Reference in a new issue