diff --git a/omega-pac/src/conditions.coffee b/omega-pac/src/conditions.coffee index 138d077..bcf84cd 100644 --- a/omega-pac/src/conditions.coffee +++ b/omega-pac/src/conditions.coffee @@ -250,7 +250,7 @@ module.exports = exports = if parts.length > 1 addr = @parseIp parts[0] prefixLen = parseInt(parts[1]) - if addr and prefixLen + if addr and not isNaN(prefixLen) cache.ip = conditionType: 'IpCondition' ip: parts[0] @@ -265,11 +265,7 @@ module.exports = exports = serverRegex = null if serverIp? if serverIp.regularExpressionString? - # TODO(felis): IPv6 regex is not fully supported by the ipv6 - # module. Even simple addresses like ::1 will fail. Shall we - # implement that instead? regexStr = serverIp.regularExpressionString(true) - console.log(regexStr) serverRegex = '\\[' + regexStr + '\\]' else server = @normalizeIp serverIp @@ -292,7 +288,7 @@ module.exports = exports = match: (condition, request, cache) -> cache = cache.analyzed return false if cache.scheme? and cache.scheme != request.scheme - return false if cache.ip? and @match cache.ip, request + return false if cache.ip? and not @match cache.ip, request if cache.host? if cache.host == '' return request.host in @localHosts @@ -381,7 +377,7 @@ module.exports = exports = mask = if cache.addr.v4 new IP.v4.Address('255.255.255.255/' + cache.addr.subnetMask) else - new IP.v6.Address(@ipv6Max + cache.addr.subnetMask) + new IP.v6.Address(@ipv6Max + '/' + cache.addr.subnetMask) cache.mask = @normalizeIp mask.startAddress() cache match: (condition, request, cache) -> @@ -392,7 +388,7 @@ module.exports = exports = return addr.isInSubnet cache.addr compile: (condition, cache) -> cache = cache.analyzed - new U2.AST_Call( + isInNetCall = new U2.AST_Call( expression: new U2.AST_SymbolRef name: 'isInNet' args: [ new U2.AST_SymbolRef name: 'host' @@ -400,6 +396,39 @@ module.exports = exports = new U2.AST_String value: cache.mask ] ) + if cache.addr.v4 then isInNetCall else + isInNetExCall = new U2.AST_Call( + expression: new U2.AST_SymbolRef name: 'isInNetEx' + args: [ + new U2.AST_SymbolRef name: 'host' + new U2.AST_String value: cache.addr.address + ] + ) + alternative = if cache.addr.subnetMask > 0 then isInNetCall else + # ::/0 ==> Just detect whether address is IPv6 (containing colons). + new U2.AST_Binary( + left: new U2.AST_Call( + expression: new U2.AST_Dot( + expression: new U2.AST_SymbolRef name: 'host' + property: 'indexOf' + ) + args: [new U2.AST_String value: ':'] + ) + operator: '>=' + right: new U2.AST_Number value: 0 + ) + new U2.AST_Conditional( + condition: new U2.AST_Binary( + left: new U2.AST_UnaryPrefix( + operator: 'typeof' + expression: new U2.AST_SymbolRef name: 'isInNetEx' + ) + operator: '===' + right: new U2.AST_String value: 'function' + ) + consequent: isInNetExCall + alternative: alternative + ) 'HostLevelsCondition': tag: (condition) -> condition.minValue + '~' + condition.maxValue analyze: (condition) -> '.'.charCodeAt 0 diff --git a/omega-pac/test/conditions.coffee b/omega-pac/test/conditions.coffee index ce2cd15..f3111aa 100644 --- a/omega-pac/test/conditions.coffee +++ b/omega-pac/test/conditions.coffee @@ -175,16 +175,95 @@ describe 'Conditions', -> pattern: 'http://127.0.0.1:8080' testCond(cond, 'http://127.0.0.1:8080/', 'match') testCond(cond, 'http://127.0.0.2:8080/', not 'match') - # TODO(felis): Not yet supported. See the code for BypassCondition. it 'should correctly support IPv6 canonicalization', -> cond = conditionType: 'BypassCondition' pattern: 'http://[0:0::1]:8080' result = Conditions.analyze(cond) - console.log(result.analyzed) testCond(cond, 'http://[::1]:8080/', 'match') testCond(cond, 'http://[1::1]:8080/', not 'match') + it 'should parse IPv4 CIDR notation', -> + cond = + conditionType: 'BypassCondition' + pattern: '192.168.0.0/16' + result = Conditions.analyze(cond).analyzed + should.exist(result.ip) + result.ip.should.eql({ + conditionType: 'IpCondition' + ip: '192.168.0.0' + prefixLength: 16 + }) + + it 'should parse IPv6 CIDR notation', -> + cond = + conditionType: 'BypassCondition' + pattern: 'fefe:13::abc/33' + result = Conditions.analyze(cond).analyzed + should.exist(result.ip) + result.ip.should.eql({ + conditionType: 'IpCondition' + ip: 'fefe:13::abc' + prefixLength: 33 + }) + + it 'should parse IPv6 CIDR notation with zero prefixLength', -> + cond = + conditionType: 'BypassCondition' + pattern: '::/0' + result = Conditions.analyze(cond).analyzed + should.exist(result.ip) + result.ip.should.eql({ + conditionType: 'IpCondition' + ip: '::' + prefixLength: 0 + }) + + describe 'IpCondition', -> + # IpCondition requires isInNetEx or isInNet function provided by the PAC + # runner, which is not available in the unit test. So We can't use testCond + # here. + it 'should support IPv4 subnet', -> + cond = + conditionType: "IpCondition" + ip: '192.168.1.1' + prefixLength: 16 + request = Conditions.requestFromUrl('http://192.168.4.4/') + Conditions.match(cond, request).should.be.true + compiled = Conditions.compile(cond).print_to_string() + compiled.should.equal('isInNet(host,"192.168.1.1","255.255.0.0")') + it 'should support IPv6 subnet', -> + cond = + conditionType: "IpCondition" + ip: 'fefe:13::abc' + prefixLength: 33 + + request = Conditions.requestFromUrl('http://[fefe:13::def]/') + Conditions.match(cond, request).should.be.true + + compiled = Conditions.compile(cond).print_to_string() + compiled_args = compiled.substr(compiled.lastIndexOf('(')) + compiled_args.should.eql('(host,"fefe:13::abc","ffff:ffff:8000::")') + it 'should support IPv6 subnet with zero prefixLength', -> + cond = + conditionType: "IpCondition" + ip: '::' + prefixLength: 0 + + request = Conditions.requestFromUrl('http://[fefe:13::def]/') + Conditions.match(cond, request).should.be.true + + compiled = Conditions.compile(cond).print_to_string() + compiled.indexOf('indexOf(').should.be.above(0) + it 'should not match domain name to IP subnet', -> + cond = + conditionType: "IpCondition" + ip: '::' + prefixLength: 0 + + request = Conditions.requestFromUrl('http://www.example.com/') + Conditions.match(cond, request).should.be.false + describe 'KeywordCondition', -> cond = conditionType: 'KeywordCondition'