LiquidProxy Lua Edition
at master 259 lines 6.7 kB view raw
1-- Inspired, and basically copied from Wowfunhappy's AquaProxy. 2-- The comments are literally 1-to-1. 3 4--[[ 5Copyright (c) 2024 Wowfunhappy 6 2015 Keith Rarick 7 8Permission is hereby granted, free of charge, to any person obtaining a copy of 9this software and associated documentation files (the "Software"), to deal in 10the Software without restriction, including without limitation the rights to 11use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 12of the Software, and to permit persons to whom the Software is furnished to do 13so, subject to the following conditions: 14 15The above copyright notice and this permission notice shall be included in all 16copies or substantial portions of the Software. 17 18THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24SOFTWARE. 25]] 26 27local bit = require "bit" 28local wr = require "coro-channel".wrapRead 29 30-- https://stackoverflow.com/a/65477617 by DarkWiiPlayer + Luatic 31local function hex(str) 32 return (str:gsub("%x%x", function(digits) return string.char(tonumber(digits, 16)) end)) 33end 34 35local function str(hex) 36 return (hex:gsub(".", function(char) return string.format("%02x", char:byte()) end)) 37end 38 39-- Go int 40local function int(hex) 41 return tonumber(str(hex), 16) 42end 43 44local function at(str, index) 45 return str:sub(index, index) 46end 47 48local const = { 49 tlsHandshakeTypeClientHello = 0x01, 50 51 tlsExtensionServerName = 0x0000, 52 tlsExtensionALPN = 0x0010, 53 tlsExtensionSupportedVersions = 0x002b, 54 55 tlsVersion10 = 0x0301, 56 tlsVersion11 = 0x0302, 57 tlsVersion12 = 0x0303, 58 tlsVersion13 = 0x0304 59} 60 61local function parseALPN(buf, info) 62 if buf:len() < 2 then 63 return 64 end 65 66 local protocolListLen = bit.bor(bit.lshift(int(at(buf, 1)), 8), int(at(buf, 2))) 67 local pos = 3 68 69 while pos < protocolListLen + 2 and pos < buf:len() do 70 local protoLen = int(at(buf, pos)) 71 pos = pos + 1 72 if pos + protoLen <= buf:len() then 73 local proto = (buf:sub(pos, pos + protoLen - 1)) 74 table.insert(info.alpnProtocols, proto) 75 76 if proto == "h2" then 77 info.supportsHTTP2 = true 78 end 79 end 80 81 pos = pos + protoLen 82 end 83end 84 85local function parseSupportedVersions(buf, info) 86 if buf:len() < 1 then 87 return 88 end 89 90 -- For ClientHello, this is a list 91 local listLen = int(at(buf, 1)) 92 local pos = 2 93 94 local i = 1 95 while i < listLen / 2 and pos + 2 <= buf:len() do 96 local version = bit.bor(bit.lshift(int(at(buf , pos)), 8), int(at(buf, pos + 1))) 97 if version == const.tlsVersion13 then 98 info.supportsTLS13 = true 99 end 100 info.tlsVersions[version] = true 101 pos = pos + 2 102 i = i + 1 103 end 104end 105 106local function parseServerName(buf, info) 107 if buf:len() < 1 then 108 return 109 end 110 111 local listLen = int(buf:sub(1, 2)) 112 local snType = int(at(buf, 3)) 113 if snType ~= 0 then return end 114 local pos = 4 115 116 while pos < listLen + 3 and pos < buf:len() do 117 local snLen = int(buf:sub(pos, pos + 1)) 118 pos = pos + 2 119 if pos + snLen <= buf:len() then 120 local sn = (buf:sub(pos, pos + snLen - 1)) 121 table.insert(info.serverNames, sn) 122 end 123 124 pos = pos + snLen 125 end 126end 127 128---@param info info 129---@return string? 130local function parseExtensions(buf, info) 131 local pos = 1 132 133 while pos + 4 <= buf:len() do 134 local extType = bit.bor(bit.lshift(int(at(buf , pos)), 8), int(at(buf, pos + 1))) 135 local extLen = bit.bor(bit.lshift(int(at(buf, pos + 2)), 8), int(at(buf, pos + 3))) 136 pos = pos + 4 137 138 if pos + extLen - 1 > buf:len() then 139 return "truncated extension" 140 end 141 142 local extData = buf:sub(pos, pos + extLen) 143 144 if extType == const.tlsExtensionALPN then 145 parseALPN(extData, info) 146 elseif extType == const.tlsExtensionSupportedVersions then 147 parseSupportedVersions(extData, info) 148 elseif extType == const.tlsExtensionServerName then 149 parseServerName(extData, info) 150 end 151 152 pos = pos + extLen 153 end 154 155 return nil 156end 157 158---@return string buffer 159---@return info? 160---@return string? err 161---@return boolean notHS 162local function tlspeek(socket) 163 ---@class info 164 local info = { 165 supportsTLS13 = false, 166 supportsHTTP2 = false, 167 ---@type table<integer, boolean> 168 tlsVersions = {}, 169 serverNames = {}, 170 alpnProtocols = {} 171 } 172 socket:read_stop() 173 local buf = wr(socket)() 174 if not buf then return "", nil, "no data received", false end 175 local len = buf:len() 176 177 local _, err, nhs = pcall(function() 178 -- Minimum size check: 5 bytes for TLS record header + 4 bytes for handshake header 179 if len < 9 then 180 return "data too short to be ClientHello", true 181 end 182 183 -- Check TLS record header 184 if at(buf, 1) ~= hex("16") then -- Handshake record type 185 return "not a TLS handshake record", true 186 end 187 188 -- Skip TLS version from record header (backwards compatibility version) 189 190 -- Get record length 191 local recordLen = bit.bor(bit.lshift(int(at(buf, 4)), 8), int(at(buf, 5))) 192 if len < recordLen + 5 then 193 return "incomplete TLS record", true 194 end 195 196 -- Parse handshake message 197 198 local pos = 6 199 if int(at(buf, pos)) ~= const.tlsHandshakeTypeClientHello then 200 return "not a ClientHello message", true 201 end 202 203 -- Skip handshake length (3 bytes...?) 204 pos = pos + 4 205 206 if len < pos + 2 then 207 return "truncated ClientHello" 208 end 209 210 info.tlsVersion = bit.bor(bit.lshift(int(at(buf , pos)), 8), int(at(buf, pos + 1))) 211 pos = pos + 2 212 213 -- Skip client random (32 bytes) 214 pos = pos + 32 215 216 -- Skip session ID 217 if len < pos + 1 then 218 return "truncated ClientHello at session ID" 219 end 220 local sessionIDLen = int(at(buf, pos)) 221 pos = pos + sessionIDLen + 1 222 223 -- Skip cipher suites 224 if len < pos + 2 then 225 return "truncated ClientHello at cipher suites" 226 end 227 local cipherSuitesLen = bit.bor(bit.lshift(int(at(buf, pos)), 8), int(at(buf, pos + 1))) 228 pos = pos + cipherSuitesLen + 2 229 230 -- Skip compression methods 231 if len < pos + 1 then 232 return "truncated ClientHello at compression" 233 end 234 local compressionLen = int(at(buf, pos)) 235 pos = pos + compressionLen + 1 236 237 if len > pos + 2 then 238 local extensionsLen = bit.bor(bit.lshift(int(at(buf, pos)), 8), int(at(buf, pos + 1))) 239 pos = pos + 2 240 241 if len >= pos + extensionsLen - 1 then 242 local err = parseExtensions(buf:sub(pos, pos + extensionsLen), info) 243 if err then 244 return err 245 end 246 end 247 end 248 249 info.isModernClient = info.supportsTLS13 or info.supportsHTTP2 250 end) 251 if err then return buf, nil, err, nhs or false end 252 253 return buf, info, nil, false 254end 255 256return { 257 peek = tlspeek, 258 const = const 259}