diff --git a/src/macro/eval/evalStdLib.ml b/src/macro/eval/evalStdLib.ml index 1191c9976cd..710e463e276 100644 --- a/src/macro/eval/evalStdLib.ml +++ b/src/macro/eval/evalStdLib.ml @@ -18,6 +18,7 @@ *) open Extlib_leftovers open Globals +open EvalArray open EvalValue open EvalEncode open EvalDecode @@ -761,6 +762,30 @@ module StdDeque = struct ) end +module StdDns = struct + + let localhost = vfun0 (fun () -> + let hostname = catch_unix_error Unix.gethostname() in + create_unknown hostname + ) + + let reverse_sync = vfun1 (fun ip_str -> + let ip_str = decode_string ip_str in + let addr = catch_unix_error Unix.inet_addr_of_string ip_str in + try + let host_entry = catch_unix_error Unix.gethostbyaddr addr in + create_unknown host_entry.h_name + with Not_found -> exc_string (Printf.sprintf "Could not reverse lookup host %s" ip_str) + ) + + let resolve_sync = vfun1 (fun hostname -> + let hostname = decode_string hostname in + let host_entry = try Unix.gethostbyname hostname with Not_found -> exc_string (Printf.sprintf "Could not resolve host %s" hostname) in + let addresses_str = Array.map (fun addr -> create_unknown (catch_unix_error Unix.string_of_inet_addr addr)) host_entry.h_addr_list in + VArray (create addresses_str) + ) +end + module StdEReg = struct open Pcre2 @@ -1382,35 +1407,6 @@ module StdGc = struct let stat = vfun0 (fun () -> encode_stats (Gc.stat())) end -module StdHost = struct - let int32_addr h = - let base = Int32.to_int (Int32.logand h 0xFFFFFFl) in - let str = Printf.sprintf "%ld.%d.%d.%d" (Int32.shift_right_logical h 24) (base lsr 16) ((base lsr 8) land 0xFF) (base land 0xFF) in - catch_unix_error Unix.inet_addr_of_string str - - let localhost = vfun0 (fun () -> - create_unknown (catch_unix_error Unix.gethostname()) - ) - - let hostReverse = vfun1 (fun ip -> - let ip = decode_i32 ip in - try create_unknown (catch_unix_error Unix.gethostbyaddr (int32_addr ip)).h_name with Not_found -> exc_string "Could not reverse host" - ) - - let hostToString = vfun1 (fun ip -> - let ip = decode_i32 ip in - create_unknown (catch_unix_error Unix.string_of_inet_addr (int32_addr ip)) - ) - - let resolve = vfun1 (fun name -> - let name = decode_string name in - let h = try Unix.gethostbyname name with Not_found -> exc_string (Printf.sprintf "Could not resolve host %s" name) in - let addr = catch_unix_error Unix.string_of_inet_addr h.h_addr_list.(0) in - let a, b, c, d = Scanf.sscanf addr "%d.%d.%d.%d" (fun a b c d -> a,b,c,d) in - vint32 (Int32.logor (Int32.shift_left (Int32.of_int a) 24) (Int32.of_int (d lor (c lsl 8) lor (b lsl 16)))) - ) -end - module StdLock = struct let this vthis = match vthis with | VInstance {ikind = ILock lock} -> lock @@ -2039,11 +2035,12 @@ module StdSocket = struct encode_instance key_eval_vm_NativeSocket ~kind:(ISocket socket) ) - let bind = vifun2 (fun vthis host port -> - let this = this vthis in - let host = decode_i32 host in + let bind = vifun2 (fun vthis ip_str port -> + let fd = this vthis in + let ip_str = decode_string ip_str in + let ip = catch_unix_error Unix.inet_addr_of_string ip_str in let port = decode_int port in - catch_unix_error Unix.bind this (ADDR_INET (StdHost.int32_addr host,port)); + catch_unix_error Unix.bind fd (ADDR_INET (ip, port)); vnull ) @@ -2052,11 +2049,12 @@ module StdSocket = struct vnull ) - let connect = vifun2 (fun vthis host port -> - let this = this vthis in - let host = decode_i32 host in + let connect = vifun2 (fun vthis ip_str port -> + let fd = this vthis in + let ip_str = decode_string ip_str in + let ip = catch_unix_error Unix.inet_addr_of_string ip_str in let port = decode_int port in - catch_unix_error (Unix.connect this) (ADDR_INET (StdHost.int32_addr host,port)); + catch_unix_error (Unix.connect fd) (ADDR_INET (ip, port)); vnull ) @@ -3556,11 +3554,10 @@ let init_standard_library builtins = "set",StdGc.set; "stat",StdGc.stat; ] []; - init_fields builtins (["sys";"net"],"Host") [ - "localhost",StdHost.localhost; - "hostReverse",StdHost.hostReverse; - "hostToString",StdHost.hostToString; - "resolve",StdHost.resolve; + init_fields builtins (["sys";"net"],"Dns") [ + "getLocalHostnameImpl", StdDns.localhost; + "reverseSyncIpv4Impl", StdDns.reverse_sync; + "resolveSyncIpv4Impl", StdDns.resolve_sync; ] []; init_fields builtins (["sys";"thread"],"Lock") [] [ "release",StdLock.release; diff --git a/std/cpp/_std/sys/net/Dns.hx b/std/cpp/_std/sys/net/Dns.hx new file mode 100644 index 00000000000..0cb2551b5f8 --- /dev/null +++ b/std/cpp/_std/sys/net/Dns.hx @@ -0,0 +1,67 @@ +/* + * Copyright (C)2005-2024 Haxe Foundation + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +package sys.net; + +import cpp.NativeSocket; + +@:coreApi +final class Dns { + private static function __init__():Void { + NativeSocket.socket_init(); + } + + public static function resolveSync(name:String):Array { + final addresses:Array = []; + try { + final ipv4 = NativeSocket.host_resolve(name); + addresses.push(Ipv4Address.fromNetworkOrderInt(ipv4)); + } catch (_) { + // Host not found, ignore + } + try { + final ipv6 = NativeSocket.host_resolve_ipv6(name); + addresses.push(@:privateAccess Ipv6Address.fromNetworkOrderBytes(ipv6)); + } catch (_) { + // Host not found, ignore + } + return addresses; + } + + public static function reverseSync(address:IpAddress):Array { + final name = switch (address) { + case V4(ipv4): + NativeSocket.host_reverse(@:privateAccess ipv4.asNetworkOrderInt()); + case V6(ipv6): + NativeSocket.host_reverse_ipv6(@:privateAccess ipv6.asNetworkOrderBytes()); + } + return if (name != null && name != "") { + [name]; + } else { + []; + }; + } + + public static function getLocalHostname():String { + return NativeSocket.host_local(); + } +} diff --git a/std/cpp/_std/sys/net/Socket.hx b/std/cpp/_std/sys/net/Socket.hx index 4c297cd3f13..a54216e86d3 100644 --- a/std/cpp/_std/sys/net/Socket.hx +++ b/std/cpp/_std/sys/net/Socket.hx @@ -22,10 +22,10 @@ package sys.net; -import haxe.io.Error; import cpp.NativeSocket; import cpp.NativeString; import cpp.Pointer; +import haxe.io.Error; private class SocketInput extends haxe.io.Input { var __s:Dynamic; @@ -168,18 +168,23 @@ class Socket { public function connect(host:Host, port:Int):Void { try { - if (host.ip == 0 && host.host != "0.0.0.0") { - // hack, hack, hack - var ipv6:haxe.io.BytesData = Reflect.field(host, "ipv6"); - if (ipv6 != null) { - close(); - __s = NativeSocket.socket_new_ip(false, true); - init(); - NativeSocket.socket_connect_ipv6(__s, ipv6, port); - } else - throw "Unresolved host"; - } else - NativeSocket.socket_connect(__s, host.ip, port); + final addresses = host.addresses; + if (addresses.length == 0) { + throw "Unresolved host"; + } + + final addr = addresses[0]; // TODO: family preferences? + switch (addr) { + case V4(ipv4): + final ip = @:privateAccess ipv4.asNetworkOrderInt(); + NativeSocket.socket_connect(this.__s, ip, port); + case V6(ipv6): + this.close(); + this.__s = NativeSocket.socket_new_ip(false, true); + this.init(); + final ip = @:privateAccess ipv6.asNetworkOrderBytes(); + NativeSocket.socket_connect_ipv6(this.__s, ip, port); + } } catch (s:String) { if (s == "Invalid socket handle") throw "Failed to connect on " + host.toString() + ":" + port; @@ -228,7 +233,7 @@ class Socket { return null; } var h = new Host("127.0.0.1"); - untyped h.ip = a[0]; + @:privateAccess h.addresses = [V4(cast a[0])]; return {host: h, port: a[1]}; } @@ -238,7 +243,7 @@ class Socket { return null; } var h = new Host("127.0.0.1"); - untyped h.ip = a[0]; + @:privateAccess h.addresses = [V4(cast a[0])]; return {host: h, port: a[1]}; } @@ -266,8 +271,11 @@ class Socket { var neko_array = NativeSocket.socket_select(read, write, others, timeout); if (neko_array == null) throw "Select error"; - return @:fixed { - read:neko_array[0], write:neko_array[1], others:neko_array[2] - }; + return @:fixed + { + read: neko_array[0], + write: neko_array[1], + others: neko_array[2] + }; } } diff --git a/std/cpp/_std/sys/net/Host.hx b/std/eval/_std/sys/net/Dns.hx similarity index 56% rename from std/cpp/_std/sys/net/Host.hx rename to std/eval/_std/sys/net/Dns.hx index f403b96e0cc..732d14cd916 100644 --- a/std/cpp/_std/sys/net/Host.hx +++ b/std/eval/_std/sys/net/Dns.hx @@ -1,5 +1,5 @@ /* - * Copyright (C)2005-2019 Haxe Foundation + * Copyright (C)2005-2024 Haxe Foundation * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), @@ -22,38 +22,37 @@ package sys.net; -import cpp.NativeSocket; +import haxe.Exception; @:coreApi -class Host { - public var host(default, null):String; - - public var ip(default, null):Int; - - private var ipv6(default, null):haxe.io.BytesData; - - public function new(name:String):Void { - host = name; +final class Dns { + public static function resolveSync(name:String):Array { try { - ip = NativeSocket.host_resolve(name); - } catch (e:Dynamic) { - ipv6 = NativeSocket.host_resolve_ipv6(name); + final addresses:Array = []; + for (addressStr in resolveSyncIpv4Impl(name)) { + final ipAddr = IpAddress.tryParse(addressStr); + if (ipAddr != null) { + addresses.push(ipAddr); + } + } + return addresses; + } catch (e:Exception) { + trace(e.details()); + return []; } } - public function toString():String { - return ipv6 == null ? NativeSocket.host_to_string(ip) : NativeSocket.host_to_string_ipv6(ipv6); + public static function reverseSync(address:IpAddress):Array { + return [reverseSyncIpv4Impl(address.toString())]; } - public function reverse():String { - return ipv6 == null ? NativeSocket.host_reverse(ip) : NativeSocket.host_reverse_ipv6(ipv6); + public static function getLocalHostname():String { + return getLocalHostnameImpl(); } - public static function localhost():String { - return NativeSocket.host_local(); - } + private static extern function resolveSyncIpv4Impl(name:String):Array; - static function __init__():Void { - NativeSocket.socket_init(); - } + private static extern function reverseSyncIpv4Impl(ipStr:String):String; + + private static extern function getLocalHostnameImpl():String; } diff --git a/std/eval/_std/sys/net/Socket.hx b/std/eval/_std/sys/net/Socket.hx index 9833a2172c3..d930738dba7 100644 --- a/std/eval/_std/sys/net/Socket.hx +++ b/std/eval/_std/sys/net/Socket.hx @@ -22,8 +22,8 @@ package sys.net; -import haxe.io.Error; import eval.vm.NativeSocket; +import haxe.io.Error; private class SocketOutput extends haxe.io.Output { var socket:NativeSocket; @@ -132,7 +132,8 @@ class Socket { } public function connect(host:Host, port:Int):Void { - socket.connect(host.ip, port); + final address = @:privateAccess host.getAddressesSorted(PreferIPv4)[0]; + socket.connect(address.toString(), port); } public function listen(connections:Int):Void { @@ -144,7 +145,8 @@ class Socket { } public function bind(host:Host, port:Int):Void { - socket.bind(host.ip, port); + final address = @:privateAccess host.getAddressesSorted(PreferIPv4)[0]; + socket.bind(address.toString(), port); } public function accept():Socket { @@ -156,17 +158,19 @@ class Socket { @:access(sys.net.Host.init) public function peer():{host:Host, port:Int} { - var info = socket.peer(); - var host:Host = Type.createEmptyInstance(Host); - host.init(info.ip); + final info = socket.peer(); + final ipv4 = Ipv4Address.fromNetworkOrderInt(info.ip); + final host = new Host(ipv4.toString()); + final port = info.port; return {host: host, port: info.port}; } @:access(sys.net.Host.init) public function host():{host:Host, port:Int} { - var info = socket.host(); - var host:Host = Type.createEmptyInstance(Host); - host.init(info.ip); + final info = socket.host(); + final ipv4 = Ipv4Address.fromNetworkOrderInt(info.ip); + final host = new Host(ipv4.toString()); + final port = info.port; return {host: host, port: info.port}; } diff --git a/std/eval/_std/sys/ssl/Socket.hx b/std/eval/_std/sys/ssl/Socket.hx index 96a3c7dff6d..be532ac6cb3 100644 --- a/std/eval/_std/sys/ssl/Socket.hx +++ b/std/eval/_std/sys/ssl/Socket.hx @@ -1,9 +1,10 @@ package sys.ssl; -import haxe.io.Bytes; import eval.vm.NativeSocket; +import haxe.io.Bytes; import mbedtls.Config; import mbedtls.Ssl; +import sys.net.UnsupportedFamilyException; private class SocketInput extends haxe.io.Input { @:allow(sys.ssl.Socket) private var socket:Socket; @@ -119,7 +120,10 @@ class Socket extends sys.net.Socket { hostname = host.host; if (hostname != null) ssl.set_hostname(hostname); - socket.connect(host.ip, port); + + final address = host.addresses[0]; + socket.connect(address.toString(), port); + if (isBlocking) handshake(); } @@ -160,8 +164,8 @@ class Socket extends sys.net.Socket { public override function bind(host:sys.net.Host, port:Int):Void { conf = buildConfig(true); - - socket.bind(host.ip, port); + final address = host.addresses[0]; + socket.bind(address.toString(), port); } public override function accept():Socket { diff --git a/std/eval/vm/NativeSocket.hx b/std/eval/vm/NativeSocket.hx index 1943030f975..aaa520f0c72 100644 --- a/std/eval/vm/NativeSocket.hx +++ b/std/eval/vm/NativeSocket.hx @@ -27,9 +27,9 @@ import sys.net.Socket; extern class NativeSocket { function new():Void; function accept():NativeSocket; - function bind(host:Int, port:Int):Void; + function bind(ipStr:String, port:Int):Void; function close():Void; - function connect(host:Int, port:Int):Void; + function connect(ipStr:String, port:Int):Void; function host():{ip:Int, port:Int}; function listen(connections:Int):Void; function peer():{ip:Int, port:Int}; diff --git a/std/hl/_std/sys/net/Dns.hx b/std/hl/_std/sys/net/Dns.hx new file mode 100644 index 00000000000..2e7ba54a953 --- /dev/null +++ b/std/hl/_std/sys/net/Dns.hx @@ -0,0 +1,72 @@ +/* + * Copyright (C)2005-2024 Haxe Foundation + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +package sys.net; + +@:coreApi +final class Dns { + public static function resolveSync(name:String):Array { + final ipv4 = resolveSyncIpv4Impl(@:privateAccess name.bytes.utf16ToUtf8(0, null)); + return if (ipv4 == -1) { + []; + } else { + [Ipv4Address.fromNetworkOrderInt(ipv4)]; + }; + } + + public static function reverseSync(address:IpAddress):Array { + switch (address) { + case V4(ipv4): + final nameBytes = reverseSyncIpv4Impl(@:privateAccess ipv4.asNetworkOrderInt()); + if (nameBytes != null) { + final name = @:privateAccess String.fromUTF8(nameBytes); + return [name]; + } + case V6(_): + throw new UnsupportedFamilyException("Hashlink does not support reverse lookup for IPv6 addresses"); + } + return []; + } + + public static function getLocalHostname():String { + final nameBytes = getLocalHostnameImpl(); + if (nameBytes != null) { + return @:privateAccess String.fromUTF8(nameBytes); + } + throw new Sys.SysError("Could not get local hostname"); + } + + @:hlNative("std", "host_resolve") + private static function resolveSyncIpv4Impl(name:hl.Bytes):Int { + return -1; + } + + @:hlNative("std", "host_reverse") + private static function reverseSyncIpv4Impl(ipv4:Int):hl.Bytes { + return hl.Bytes.fromAddress(0i64); + } + + @:hlNative("std", "host_local") + private static function getLocalHostnameImpl():hl.Bytes { + return hl.Bytes.fromAddress(0i64); + } +} diff --git a/std/hl/_std/sys/net/Socket.hx b/std/hl/_std/sys/net/Socket.hx index 0cd7113089c..50267757249 100644 --- a/std/hl/_std/sys/net/Socket.hx +++ b/std/hl/_std/sys/net/Socket.hx @@ -187,18 +187,18 @@ class Socket { var ip = 0, port = 0; if (!socket_peer(__s, ip, port)) return null; - var h:Host = untyped $new(Host); - @:privateAccess h.ip = ip; - return {host: h, port: port}; + final host:Host = untyped $new(Host); + @:privateAccess host.addresses = [Ipv4Address.fromNetworkOrderInt(ip)]; + return {host: host, port: port}; } public function host():{host:Host, port:Int} { var ip = 0, port = 0; if (!socket_host(__s, ip, port)) return null; - var h:Host = untyped $new(Host); - @:privateAccess h.ip = ip; - return {host: h, port: port}; + final host:Host = untyped $new(Host); + @:privateAccess host.addresses = [Ipv4Address.fromNetworkOrderInt(ip)]; + return {host: host, port: port}; } public function setTimeout(timeout:Float):Void { diff --git a/std/java/_std/sys/net/Host.hx b/std/java/_std/sys/net/Dns.hx similarity index 55% rename from std/java/_std/sys/net/Host.hx rename to std/java/_std/sys/net/Dns.hx index 320b69e317a..fb44883cd8b 100644 --- a/std/java/_std/sys/net/Host.hx +++ b/std/java/_std/sys/net/Dns.hx @@ -1,5 +1,5 @@ /* - * Copyright (C)2005-2019 Haxe Foundation + * Copyright (C)2005-2024 Haxe Foundation * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), @@ -22,37 +22,41 @@ package sys.net; +import haxe.Exception; +import java.net.Inet4Address; +import java.net.Inet6Address; import java.net.InetAddress; -class Host { - public var host(default, null):String; - public var ip(default, null):Int; - - @:allow(sys.net) private var wrapped:InetAddress; - - public function new(name:String):Void { - host = name; - try - this.wrapped = InetAddress.getByName(name) - catch (e:Dynamic) - throw e; - var rawIp = wrapped.getAddress(); - // network byte order assumed - this.ip = cast(rawIp[3], Int) | (cast(rawIp[2], Int) << 8) | (cast(rawIp[1], Int) << 16) | (cast(rawIp[0], Int) << 24); - } +using Lambda; + +@:coreApi +final class Dns { + public static function resolveSync(name:String):Array { + @:nullSafety(Off) var nativeAddrs:Array = null; - public function toString():String { - return wrapped.getHostAddress(); + try { + nativeAddrs = @:privateAccess Array.ofNative(InetAddress.getAllByName(name)); + } catch (_) { + return []; + } + + final addresses:Array = []; + for (nativeAddr in nativeAddrs) { + final ipAddr = IpAddress.tryParse(nativeAddr.getHostAddress()); + if (ipAddr != null) { + addresses.push(ipAddr); + } + } + + return addresses; } - public function reverse():String { - return wrapped.getHostName(); + public static function reverseSync(address:IpAddress):Array { + final nativeAddr = InetAddress.getByName(address.toString()); + return [nativeAddr.getHostName()]; } - public static function localhost():String { - try { - return InetAddress.getLocalHost().getHostName(); - } catch (e:Dynamic) - throw e; + public static function getLocalHostname():String { + return InetAddress.getLocalHost().getHostName(); } } diff --git a/std/java/_std/sys/net/Socket.hx b/std/java/_std/sys/net/Socket.hx index 90b9d77021e..e942f6a52ec 100644 --- a/std/java/_std/sys/net/Socket.hx +++ b/std/java/_std/sys/net/Socket.hx @@ -22,6 +22,7 @@ package sys.net; +import java.net.InetAddress; import java.net.InetSocketAddress; @:coreApi @@ -67,7 +68,9 @@ class Socket { public function connect(host:Host, port:Int):Void { try { - sock.connect(new InetSocketAddress(host.wrapped, port)); + final address = host.addresses[0]; + final nativeAddr = InetAddress.getByName(address.toString()); + this.sock.connect(new InetSocketAddress(nativeAddr, port)); this.output = new java.io.NativeOutput(sock.getOutputStream()); this.input = new java.io.NativeInput(sock.getInputStream()); } catch (e:Dynamic) @@ -94,11 +97,12 @@ class Socket { } public function bind(host:Host, port:Int):Void { - if (boundAddr != null) { - if (server.isBound()) - throw "Already bound"; + if (this.server.isBound() && this.boundAddr != null) { + throw "Already bound"; } - this.boundAddr = new java.net.InetSocketAddress(host.wrapped, port); + + final address = host.addresses[0]; + this.boundAddr = new java.net.InetSocketAddress(address.toString(), port); } public function accept():Socket { @@ -113,25 +117,17 @@ class Socket { } public function peer():{host:Host, port:Int} { - var rem = sock.getInetAddress(); - if (rem == null) - return null; - - var host = new Host(null); - host.wrapped = rem; - return {host: host, port: sock.getPort()}; + final nativePeerAddr = sock.getInetAddress() ?? return null; + final host = new Host(nativePeerAddr.getHostAddress()); + final port = this.sock.getPort(); + return {host: host, port: port}; } public function host():{host:Host, port:Int} { - var local = sock.getLocalAddress(); - var host = new Host(null); - host.wrapped = local; - - if (boundAddr != null) { - return {host: host, port: server.getLocalPort()}; - } - - return {host: host, port: sock.getLocalPort()}; + final nativeAddr = sock.getLocalAddress(); + final host = new Host(nativeAddr.getHostAddress()); + final port = this.boundAddr != null ? this.server.getLocalPort() : this.sock.getLocalPort(); + return {host: host, port: port}; } public function setTimeout(timeout:Float):Void { diff --git a/std/neko/_std/sys/net/Host.hx b/std/lua/_std/sys/net/Dns.hx similarity index 52% rename from std/neko/_std/sys/net/Host.hx rename to std/lua/_std/sys/net/Dns.hx index 4ff7a5cff24..44ecf3883f4 100644 --- a/std/neko/_std/sys/net/Host.hx +++ b/std/lua/_std/sys/net/Dns.hx @@ -1,5 +1,5 @@ /* - * Copyright (C)2005-2019 Haxe Foundation + * Copyright (C)2005-2024 Haxe Foundation * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), @@ -22,36 +22,43 @@ package sys.net; -@:coreApi -@:keepInit -class Host { - public var host(default, null):String; - - public var ip(default, null):Int; - - public function new(name:String):Void { - host = name; - ip = host_resolve(untyped name.__s); - } +import lua.PairTools; +import lua.lib.luv.Os in LuvOs; +import lua.lib.luv.net.Dns in LuvDns; - public function toString():String { - return new String(host_to_string(ip)); - } +@:coreApi +final class Dns { + public static function resolveSync(name:String):Array { + final infos = LuvDns.getaddrinfo(name, null, null).result; + if (infos == null) { + return []; + } - public function reverse():String { - return new String(host_reverse(ip)); + final addresses:Array = []; + PairTools.ipairsEach(infos, (_, addrinfo) -> { + switch (addrinfo.family) { + case "inet": + final ipv4 = Ipv4Address.tryParse(addrinfo.addr); + if (ipv4 != null) { + addresses.push(ipv4); + } + case "inet6": + final ipv6 = Ipv6Address.tryParse(addrinfo.addr); + if (ipv6 != null) { + addresses.push(ipv6); + } + case _: + } + }); + return addresses; } - public static function localhost():String { - return new String(host_local()); + public static function reverseSync(address:IpAddress):Array { + final reversed = LuvDns.getnameinfo({ip: address.toString()}).result; + return reversed != null ? [reversed] : []; } - static function __init__():Void { - neko.Lib.load("std", "socket_init", 0)(); + public static function getLocalHostname():String { + return LuvOs.gethostname(); } - - private static var host_resolve = neko.Lib.load("std", "host_resolve", 1); - private static var host_reverse = neko.Lib.load("std", "host_reverse", 1); - private static var host_to_string = neko.Lib.load("std", "host_to_string", 1); - private static var host_local = neko.Lib.load("std", "host_local", 0); } diff --git a/std/neko/_std/sys/net/Dns.hx b/std/neko/_std/sys/net/Dns.hx new file mode 100644 index 00000000000..f3c64a417fd --- /dev/null +++ b/std/neko/_std/sys/net/Dns.hx @@ -0,0 +1,70 @@ +/* + * Copyright (C)2005-2024 Haxe Foundation + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +package sys.net; + +import haxe.exceptions.NotImplementedException; +import neko.Lib; +import neko.NativeString; + +@:coreApi +@:keepInit +final class Dns { + private static function __init__():Void { + Dns.resolveSyncIpv4Impl = cast Lib.load("std", "host_resolve", 1); + Dns.reverseSyncIpv4Impl = cast Lib.load("std", "host_reverse", 1); + Dns.getLocalHostnameImpl = cast Lib.load("std", "host_local", 0); + } + + public static function resolveSync(name:String):Array { + final ipv4 = resolveSyncIpv4Impl(NativeString.ofString(name)); + final address = Ipv4Address.fromNetworkOrderInt(ipv4); + return [address]; + } + + private static dynamic function resolveSyncIpv4Impl(name:NativeString):Int { + throw new NotImplementedException("Neko function was not loaded"); + } + + public static function reverseSync(address:IpAddress):Array { + return switch (address) { + case V4(addr): + final ipv4 = addr.asNetworkOrderInt(); + final reversed = reverseSyncIpv4Impl(ipv4); + [NativeString.toString(reversed)]; + case _: + throw new UnsupportedFamilyException("Neko cannot reverse lookup IPv6 addresses"); + }; + } + + private static dynamic function reverseSyncIpv4Impl(ipv4:Int):NativeString { + throw new NotImplementedException("Neko function was not loaded"); + } + + public static function getLocalHostname():String { + return NativeString.toString(getLocalHostnameImpl()); + } + + private static dynamic function getLocalHostnameImpl():NativeString { + throw new NotImplementedException("Neko function was not loaded"); + } +} diff --git a/std/neko/_std/sys/net/Socket.hx b/std/neko/_std/sys/net/Socket.hx index 4ad9756be2a..145af116990 100644 --- a/std/neko/_std/sys/net/Socket.hx +++ b/std/neko/_std/sys/net/Socket.hx @@ -22,7 +22,10 @@ package sys.net; +import haxe.Int32; +import haxe.exceptions.NotImplementedException; import haxe.io.Error; +import neko.NativeArray; @:callable @:coreType @@ -117,6 +120,7 @@ private class SocketInput extends haxe.io.Input { } @:coreApi +@:keepInit class Socket { private var __s:SocketHandle; @@ -124,13 +128,34 @@ class Socket { public var output(default, null):haxe.io.Output; public var custom:Dynamic; + private static function __init__():Void { + Socket.socket_new = cast neko.Lib.load("std", "socket_new", 1); + Socket.socket_close = cast neko.Lib.load("std", "socket_close", 1); + Socket.socket_write = cast neko.Lib.load("std", "socket_write", 2); + Socket.socket_read = cast neko.Lib.load("std", "socket_read", 1); + Socket.socket_connect = cast neko.Lib.load("std", "socket_connect", 3); + Socket.socket_listen = cast neko.Lib.load("std", "socket_listen", 2); + Socket.socket_select = cast neko.Lib.load("std", "socket_select", 4); + Socket.socket_bind = cast neko.Lib.load("std", "socket_bind", 3); + Socket.socket_accept = cast neko.Lib.load("std", "socket_accept", 1); + Socket.socket_peer = cast neko.Lib.load("std", "socket_peer", 1); + Socket.socket_host = cast neko.Lib.load("std", "socket_host", 1); + Socket.socket_set_timeout = cast neko.Lib.load("std", "socket_set_timeout", 2); + Socket.socket_shutdown = cast neko.Lib.load("std", "socket_shutdown", 3); + Socket.socket_set_blocking = cast neko.Lib.load("std", "socket_set_blocking", 2); + Socket.socket_set_fast_send = cast neko.Lib.loadLazy("std", "socket_set_fast_send", 2); + Socket.int32_new = cast neko.Lib.load("std", "int32_new", 1); + neko.Lib.load("std", "socket_init", 0)(); + } + public function new():Void { init(); } private function init():Void { - if (__s == null) - __s = socket_new(false); + if (this.__s == null) { + this.__s = socket_new(false); + } input = new SocketInput(__s); output = new SocketOutput(__s); } @@ -154,16 +179,24 @@ class Socket { } public function connect(host:Host, port:Int):Void { - try { - socket_connect(__s, host.ip, port); - } catch (s:String) { - if (s == "std@socket_connect") - throw "Failed to connect on " + host.toString() + ":" + port; - else if (s == "Blocking") { - // Do nothing, this is not a real error, it simply indicates - // that a non-blocking connect is in progress - } else - neko.Lib.rethrow(s); + final address = @:privateAccess host.getAddressesSorted(PreferIPv4)[0]; + switch (address) { + case V4(addr): + final ipv4 = addr.asNetworkOrderInt(); + try { + socket_connect(__s, int32_new(ipv4), port); + } catch (s:String) { + if (s == "std@socket_connect") + throw 'Failed to connect on $addr:$port'; + else if (s == "Blocking") { + // Do nothing, this is not a real error, it simply indicates + // that a non-blocking connect is in progress + } else { + neko.Lib.rethrow(s); + } + } + case V6(_): + throw new UnsupportedFamilyException("Neko does not support connecting to IPv6 addresses"); } } @@ -176,7 +209,14 @@ class Socket { } public function bind(host:Host, port:Int):Void { - socket_bind(__s, host.ip, port); + final address = @:privateAccess host.getAddressesSorted(PreferIPv4)[0]; + switch (address) { + case V4(addr): + final ipv4:Int32 = addr.asNetworkOrderInt(); + socket_bind(this.__s, int32_new(ipv4), port); + case V6(_): + throw new UnsupportedFamilyException("Neko does not support binding to IPv6 interfaces"); + } } public function accept():Socket { @@ -193,9 +233,10 @@ class Socket { if (a == null) { return null; } - var h = new Host("127.0.0.1"); - untyped h.ip = a[0]; - return {host: h, port: a[1]}; + final ipv4 = Ipv4Address.fromNetworkOrderInt(cast a[0]); + final host = new Host(ipv4.toString()); + final port:Int = cast a[1]; + return {host: host, port: port}; } public function host():{host:Host, port:Int} { @@ -203,9 +244,10 @@ class Socket { if (a == null) { return null; } - var h = new Host("127.0.0.1"); - untyped h.ip = a[0]; - return {host: h, port: a[1]}; + final ipv4 = Ipv4Address.fromNetworkOrderInt(cast a[0]); + final host = new Host(ipv4.toString()); + final port:Int = cast a[1]; + return {host: host, port: port}; } public function setTimeout(timeout:Float):Void { @@ -227,25 +269,27 @@ class Socket { public static function select(read:Array, write:Array, others:Array, ?timeout:Float):{read:Array, write:Array, others:Array} { var c = untyped __dollar__hnew(1); - var f = function(a:Array) { - if (a == null) + final f:(Array) -> NativeArray = (a) -> { + if (a == null) { return null; - untyped { - var r = __dollar__amake(a.length); - var i = 0; - while (i < a.length) { - r[i] = a[i].__s; - __dollar__hadd(c, a[i].__s, a[i]); - i += 1; - } - return r; } + + final length = a.length; + final r:NativeArray = NativeArray.alloc(length); + var i = 0; + while (i < length) untyped { + r[i] = a[i].__s; + __dollar__hadd(c, a[i].__s, a[i]); + i += 1; + } + return r; } var neko_array = socket_select(f(read), f(write), f(others), timeout); - var g = function(a):Array { - if (a == null) + final g = function(a):Array { + if (a == null) { return null; + } var r = new Array(); var i = 0; @@ -266,19 +310,68 @@ class Socket { }; } - private static var socket_new = neko.Lib.load("std", "socket_new", 1); - private static var socket_close = neko.Lib.load("std", "socket_close", 1); - private static var socket_write = neko.Lib.load("std", "socket_write", 2); - private static var socket_read = neko.Lib.load("std", "socket_read", 1); - private static var socket_connect = neko.Lib.load("std", "socket_connect", 3); - private static var socket_listen = neko.Lib.load("std", "socket_listen", 2); - private static var socket_select = neko.Lib.load("std", "socket_select", 4); - private static var socket_bind = neko.Lib.load("std", "socket_bind", 3); - private static var socket_accept = neko.Lib.load("std", "socket_accept", 1); - private static var socket_peer = neko.Lib.load("std", "socket_peer", 1); - private static var socket_host = neko.Lib.load("std", "socket_host", 1); - private static var socket_set_timeout = neko.Lib.load("std", "socket_set_timeout", 2); - private static var socket_shutdown = neko.Lib.load("std", "socket_shutdown", 3); - private static var socket_set_blocking = neko.Lib.load("std", "socket_set_blocking", 2); - private static var socket_set_fast_send = neko.Lib.loadLazy("std", "socket_set_fast_send", 2); + private static dynamic function socket_new(datagram:Bool):SocketHandle { + throw new NotImplementedException("Neko function was not loaded"); + } + + private static dynamic function socket_close(handle:SocketHandle):Void { + throw new NotImplementedException("Neko function was not loaded"); + } + + private static dynamic function socket_write(handle:SocketHandle, stringData:Any):Void { + throw new NotImplementedException("Neko function was not loaded"); + } + + private static dynamic function socket_read(handle:SocketHandle):Dynamic { + throw new NotImplementedException("Neko function was not loaded"); + } + + private static dynamic function socket_connect(handle:SocketHandle, ipv4:Int32, port:Int):Void { + throw new NotImplementedException("Neko function was not loaded"); + } + + private static dynamic function socket_listen(handle:SocketHandle, connections:Int):Void { + throw new NotImplementedException("Neko function was not loaded"); + } + + private static dynamic function socket_select(readHandlesArray:NativeArray, writeHandlesArray:NativeArray, + otherHandlesArray:NativeArray, timeout:Float):NativeArray { + throw new NotImplementedException("Neko function was not loaded"); + } + + private static dynamic function socket_bind(handle:SocketHandle, ipv4:Int32, port:Int):Void { + throw new NotImplementedException("Neko function was not loaded"); + } + + private static dynamic function socket_accept(serverHandle:SocketHandle):SocketHandle { + throw new NotImplementedException("Neko function was not loaded"); + } + + private static dynamic function socket_peer(handle:SocketHandle):Dynamic { + throw new NotImplementedException("Neko function was not loaded"); + } + + private static dynamic function socket_host(handle:SocketHandle):Dynamic { + throw new NotImplementedException("Neko function was not loaded"); + } + + private static dynamic function socket_set_timeout(handle:SocketHandle, timeout:Float):Void { + throw new NotImplementedException("Neko function was not loaded"); + } + + private static dynamic function socket_shutdown(handle:SocketHandle, read:Bool, write:Bool):Void { + throw new NotImplementedException("Neko function was not loaded"); + } + + private static dynamic function socket_set_blocking(handle:SocketHandle, blocking:Bool):Void { + throw new NotImplementedException("Neko function was not loaded"); + } + + private static dynamic function socket_set_fast_send(handle:SocketHandle, fastSend:Bool):Void { + throw new NotImplementedException("Neko function was not loaded"); + } + + private static dynamic function int32_new(v:Int32):Int32 { + throw new NotImplementedException("Neko function was not loaded"); + } } diff --git a/std/neko/_std/sys/ssl/Socket.hx b/std/neko/_std/sys/ssl/Socket.hx index 17b0a571e46..96a79d95292 100644 --- a/std/neko/_std/sys/ssl/Socket.hx +++ b/std/neko/_std/sys/ssl/Socket.hx @@ -22,6 +22,8 @@ package sys.ssl; +import sys.net.UnsupportedFamilyException; + private typedef SocketHandle = Dynamic; private typedef CTX = Dynamic; private typedef SSL = Dynamic; @@ -152,24 +154,34 @@ class Socket extends sys.net.Socket { } public override function connect(host:sys.net.Host, port:Int):Void { - try { - ctx = buildSSLContext(false); - ssl = ssl_new(ctx); - ssl_set_socket(ssl, __s); - handshakeDone = false; - if (hostname == null) - hostname = host.host; - if (hostname != null) - ssl_set_hostname(ssl, untyped hostname.__s); - socket_connect(__s, host.ip, port); - handshake(); - } catch (s:String) { - if (s == "std@socket_connect") - throw "Failed to connect on " + host.host + ":" + port; - else - neko.Lib.rethrow(s); - } catch (e:Dynamic) { - neko.Lib.rethrow(e); + final address = @:privateAccess host.getAddressesSorted(PreferIPv4)[0]; + switch (address) { + case V4(addr): + try { + ctx = buildSSLContext(false); + ssl = ssl_new(ctx); + ssl_set_socket(ssl, __s); + handshakeDone = false; + + this.hostname ??= host.host; + if (this.hostname != null) { + ssl_set_hostname(ssl, untyped this.hostname.__s); + } + + final ipv4 = @:privateAccess addr.asNetworkOrderInt(); + socket_connect(this.__s, ipv4, port); + handshake(); + } catch (s:String) { + if (s == "std@socket_connect") { + throw 'Failed to connect on $addr:$port'; + } else { + neko.Lib.rethrow(s); + } + } catch (e:Dynamic) { + neko.Lib.rethrow(e); + } + case V6(_): + throw new UnsupportedFamilyException("Neko does not support connecting to IPv6 addresses"); } } @@ -236,8 +248,14 @@ class Socket extends sys.net.Socket { public override function bind(host:sys.net.Host, port:Int):Void { ctx = buildSSLContext(true); - - socket_bind(__s, host.ip, port); + final address = @:privateAccess host.getAddressesSorted(PreferIPv4)[0]; + switch (address) { + case V4(addr): + final ipv4 = @:privateAccess addr.asNetworkOrderInt(); + socket_bind(this.__s, ipv4, port); + case V6(_): + throw new UnsupportedFamilyException("Neko does not support binding to IPv6 interfaces"); + } } public override function accept():Socket { @@ -271,14 +289,18 @@ class Socket extends sys.net.Socket { var servername = new String(cast servername); for (c in altSNIContexts) { if (c.match(servername)) - return @:privateAccess { - key:c.key.__k, cert:c.cert.__x - }; + return @:privateAccess + { + key: c.key.__k, + cert: c.cert.__x + }; } if (ownKey != null && ownCert != null) - return @:privateAccess { - key:ownKey.__k, cert:ownCert.__x - }; + return @:privateAccess + { + key: ownKey.__k, + cert: ownCert.__x + }; return null; } conf_set_servername_callback(ctx, sniCallback); diff --git a/std/php/Global.hx b/std/php/Global.hx index ab4b89c2573..68c0543a995 100644 --- a/std/php/Global.hx +++ b/std/php/Global.hx @@ -1509,6 +1509,11 @@ extern class Global { **/ static function gethostbyname(hostname:String):String; + /** + @see https://php.net/manual/en/function.gethostbynamel.php + **/ + static function gethostbynamel(hostname:String):EitherType, Bool>; + /** @see http://php.net/manual/en/function.gethostbyaddr.php **/ diff --git a/std/lua/_std/sys/net/Host.hx b/std/php/_std/sys/net/Dns.hx similarity index 54% rename from std/lua/_std/sys/net/Host.hx rename to std/php/_std/sys/net/Dns.hx index c915bc31949..904ba867c0f 100644 --- a/std/lua/_std/sys/net/Host.hx +++ b/std/php/_std/sys/net/Dns.hx @@ -1,5 +1,5 @@ /* - * Copyright (C)2005-2019 Haxe Foundation + * Copyright (C)2005-2024 Haxe Foundation * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), @@ -22,50 +22,43 @@ package sys.net; -import haxe.io.Bytes; -import haxe.io.BytesInput; +import php.Global.gethostbyaddr; +import php.Global.gethostbynamel; +import php.NativeIndexedArray; +import php.SuperGlobal._SERVER; -import lua.NativeStringTools.find; +using Lambda; -import lua.lib.luv.net.Dns; -import lua.lib.luv.Os; - -@:coreapi -class Host { - public var host(default, null):String; - - public var ip(default, null):Int; - - var _ip:String; - - public function new(name:String):Void { - host = name; - if (find(name, "(%d+)%.(%d+)%.(%d+)%.(%d+)").begin != null) { - _ip = name; - } else { - var res = lua.lib.luv.net.Dns.getaddrinfo(name); - if (res.result == null) - throw "Unrecognized node name"; - _ip = res.result[1].addr; - if (_ip == "::1") - _ip = "127.0.0.0"; +@:coreApi +final class Dns { + public static function resolveSync(name:String):Array { + final addressesStr = gethostbynamel(name); + if (addressesStr == false) { + return []; } - var num = 0; - for (a in _ip.split(".")) { - num = num * 256 + lua.Lua.tonumber(a); + + final addresses:Array = []; + for (addrStr in (cast addressesStr : NativeIndexedArray)) { + final ipAddr = IpAddress.tryParse(addrStr); + if (ipAddr != null) { + addresses.push(ipAddr); + } } - ip = num; - } - public function toString():String { - return _ip; + return addresses; } - public function reverse():String { - return Dns.getnameinfo({ip: _ip}).result; + public static function reverseSync(address:IpAddress):Array { + final addressStr = address.toString(); + final reversed = gethostbyaddr(addressStr); + return if (reversed != false && reversed != addressStr) { + [reversed]; + } else { + []; + }; } - static public function localhost():String { - return Os.gethostname(); + public static function getLocalHostname():String { + return php.Syntax.coalesce(_SERVER['HTTP_HOST'], "localhost"); } } diff --git a/std/python/_std/sys/net/Dns.hx b/std/python/_std/sys/net/Dns.hx new file mode 100644 index 00000000000..9127eff3a2d --- /dev/null +++ b/std/python/_std/sys/net/Dns.hx @@ -0,0 +1,92 @@ +/* + * Copyright (C)2005-2024 Haxe Foundation + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +package sys.net; + +import haxe.Exception; +import haxe.extern.EitherType; +import python.Syntax; +import python.Tuple; + +@:coreApi +final class Dns { + public static function resolveSync(name:String):Array { + try { + final addresses:Array = []; + for (addrinfo in PythonSocket.getaddrinfo(name, null, 0, 0, 0, 0)) { + final family:Int = addrinfo[0]; + final sockAddr:InetSockAddr = addrinfo[4]; + final ipStr:String = (cast sockAddr : Dynamic)[0]; + if (!Lambda.exists(addresses, a -> a.toString() == ipStr)) { + if (family == PythonSocket.AF_INET) { + final ipv4 = Ipv4Address.tryParse(ipStr); + if (ipv4 != null) { + addresses.push(ipv4); + } + } else if (family == PythonSocket.AF_INET6) { + final ipv6 = Ipv6Address.tryParse(ipStr); + if (ipv6 != null) { + addresses.push(ipv6); + } + } + } + } + return addresses; + } catch (e:Exception) { + return []; + } + } + + public static function reverseSync(address:IpAddress):Array { + final nameInfo = switch (address) { + case V4(ipv4): + PythonSocket.getnameinfo(Syntax.tuple(ipv4.toString(), 0), 0); + case V6(ipv6): + PythonSocket.getnameinfo(Syntax.tuple(ipv6.toString(), 0, 0, 0), 0); + }; + + final reversed = nameInfo[0]; + return if (reversed != address.toString()) { + [reversed]; + } else { + []; + }; + } + + public static function getLocalHostname():String { + return PythonSocket.gethostname(); + } +} + +private typedef Inet4SockAddr = Tuple2; +private typedef Inet6SockAddr = Tuple4; +private typedef InetSockAddr = EitherType; + +@:pythonImport("socket") +private extern class PythonSocket { + public static final AF_INET:Int; + public static final AF_INET6:Int; + public static function getaddrinfo(host:String, ?port:EitherType, family:Int, type:Int, proto:Int, + flags:Int):Array>; + public static function gethostname():String; + public static function getnameinfo(sockaddr:InetSockAddr, flags:Int):Tuple2; +} diff --git a/std/python/_std/sys/net/Socket.hx b/std/python/_std/sys/net/Socket.hx index 9815401181d..8899b05a197 100644 --- a/std/python/_std/sys/net/Socket.hx +++ b/std/python/_std/sys/net/Socket.hx @@ -22,15 +22,15 @@ package sys.net; -import haxe.io.Error; import haxe.io.Bytes; import haxe.io.BytesData; +import haxe.io.Error; import python.Exceptions; import python.Tuple; -import python.lib.socket.Socket in PSocket; +import python.lib.Select; import python.lib.Socket in PSocketModule; import python.lib.socket.Address in PAddress; -import python.lib.Select; +import python.lib.socket.Socket in PSocket; import python.lib.ssl.Errors; private class SocketInput extends haxe.io.Input { @@ -58,7 +58,7 @@ private class SocketInput extends haxe.io.Input { try { try { r = __s.recv(len, 0); - } catch(e:SSLWantReadError) { + } catch (e:SSLWantReadError) { return 0; } for (i in pos...(pos + r.length)) { @@ -101,7 +101,7 @@ private class SocketOutput extends haxe.io.Output { var r = 0; try { r = __s.send(payload, 0); - } catch(e:SSLWantWriteError) { + } catch (e:SSLWantWriteError) { return 0; } return r; @@ -136,10 +136,10 @@ private class SocketOutput extends haxe.io.Output { __s = new PSocket(); } - function __rebuildIoStreams():Void { + function __rebuildIoStreams():Void { input = new SocketInput(__s); output = new SocketOutput(__s); - } + } public function close():Void { __s.close(); @@ -154,20 +154,21 @@ private class SocketOutput extends haxe.io.Output { } public function connect(host:Host, port:Int):Void { - var host_str = host.toString(); - __s.connect(Tuple2.make(host_str, port)); + final address = @:privateAccess host.getAddressesSorted(PreferIPv4)[0]; + __s.connect(Tuple2.make(address.toString(), port)); } public function listen(connections:Int):Void { __s.listen(connections); } - public function shutdown(read:Bool, write:Bool):Void + public function shutdown(read:Bool, write:Bool):Void { __s.shutdown((read && write) ? PSocketModule.SHUT_RDWR : read ? PSocketModule.SHUT_RD : PSocketModule.SHUT_WR); + } public function bind(host:Host, port:Int):Void { - var host_str = host.toString(); - __s.bind(Tuple2.make(host_str, port)); + final address = @:privateAccess host.getAddressesSorted(PreferIPv4)[0]; + __s.bind(Tuple2.make(address.toString(), port)); } public function accept():Socket { diff --git a/std/sys/net/Address.hx b/std/sys/net/Address.hx index 5ecc0f6fca6..b4843111a3d 100644 --- a/std/sys/net/Address.hx +++ b/std/sys/net/Address.hx @@ -37,7 +37,7 @@ class Address { public function getHost() { var h = new Host("127.0.0.1"); - untyped h.ip = host; + @:privateAccess h.addresses = [V4(cast this.host)]; return h; } diff --git a/std/hl/_std/sys/net/Host.hx b/std/sys/net/Dns.hx similarity index 50% rename from std/hl/_std/sys/net/Host.hx rename to std/sys/net/Dns.hx index ed737c045de..859892f0ed1 100644 --- a/std/hl/_std/sys/net/Host.hx +++ b/std/sys/net/Dns.hx @@ -1,5 +1,5 @@ /* - * Copyright (C)2005-2019 Haxe Foundation + * Copyright (C)2005-2024 Haxe Foundation * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), @@ -22,44 +22,35 @@ package sys.net; -@:coreApi -class Host { - public var host(default, null):String; - - public var ip(default, null):Int; - - public function new(name:String):Void { - host = name; - ip = host_resolve(@:privateAccess name.bytes.utf16ToUtf8(0, null)); - if (ip == -1) - throw new Sys.SysError("Unresolved host " + name); - } - - public function toString():String { - return @:privateAccess String.fromUTF8(host_to_string(ip)); - } - - public function reverse():String { - return @:privateAccess String.fromUTF8(host_reverse(ip)); - } - - public static function localhost():String { - return @:privateAccess String.fromUTF8(host_local()); - } - - @:hlNative("std", "host_resolve") static function host_resolve(name:hl.Bytes):Int { - return 0; - } - - @:hlNative("std", "host_reverse") static function host_reverse(host:Int):hl.Bytes { - return null; - } - - @:hlNative("std", "host_to_string") static function host_to_string(host:Int):hl.Bytes { - return null; - } - - @:hlNative("std", "host_local") static function host_local():hl.Bytes { - return null; - } +/** + Provides domain name resolution tools. +**/ +extern class Dns { + /** + Tries to resolve the IP addresses for the given hostname. + + This call is blocking. + This operation may or may not retrieve from any caches, including the operating system one. + + @param name The host name to resolve. + @return An unsorted array of IP addresses. Can be empty if the host could not be resolved. + **/ + static function resolveSync(name:String):Array; + + /** + Tries to run a reverse DNS lookup on the given IP address. + + This call is blocking. + This operation may or may not retrieve from any caches, including the operating system one. + + @param address The IPv4 or IPv6 address to reverse lookup. + @return An unsorted array of hostnames pointing to the given address. + Can be empty if the address could not be resolved. + **/ + static function reverseSync(address:IpAddress):Array; + + /** + Returns the hostname (computer name, etc.) of the machine running the current process. + **/ + static function getLocalHostname():String; } diff --git a/std/python/_std/sys/net/Host.hx b/std/sys/net/DnsException.hx similarity index 73% rename from std/python/_std/sys/net/Host.hx rename to std/sys/net/DnsException.hx index d8ef9eb26a2..cab99fd99d6 100644 --- a/std/python/_std/sys/net/Host.hx +++ b/std/sys/net/DnsException.hx @@ -1,5 +1,5 @@ /* - * Copyright (C)2005-2019 Haxe Foundation + * Copyright (C)2005-2024 Haxe Foundation * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), @@ -22,26 +22,17 @@ package sys.net; -class Host { - public var host(default, null):String; - public var ip(default, null):Int; +import haxe.Exception; +import haxe.PosInfos; - var name:String; - - public function new(name:String):Void { - host = name; - this.name = name; - } - - public function toString():String { - return name; - } - - public function reverse():String { - return ""; - } - - public static function localhost():String { - return ""; +/** + This exception is thrown when domain name resolution unexpectedly fails. +**/ +class DnsException extends Exception { + /** + Creates a new instance of `DnsException`. + **/ + public function new(message:String, ?previous:Exception) { + super(message, previous); } } diff --git a/std/sys/net/Host.hx b/std/sys/net/Host.hx index 9cd54fda795..cc7acc5c9d1 100644 --- a/std/sys/net/Host.hx +++ b/std/sys/net/Host.hx @@ -22,38 +22,138 @@ package sys.net; +import haxe.Exception; +import sys.net.IpAddress; + /** - A given IP host name. + Stores address information about a given internet host. **/ -extern class Host { +class Host { /** - The provided host string. + The original provided hostname. **/ - var host(default, null):String; + public var host(default, null):String; /** - The actual IP corresponding to the host. + Known IP addresses corresponding to this host. Not in any particular order. **/ - var ip(default, null):Int; + public var addresses(default, null):Array; /** - Creates a new Host : the name can be an IP in the form "127.0.0.1" or an host name such as "google.com", in which case - the corresponding IP address is resolved using DNS. An exception occur if the host name could not be found. + If any IPv4 address is associated with this host, + returns an arbitrary one as a big-endian integer. + Throws otherwise. **/ - function new(name:String):Void; + @:noCompletion + @:deprecated("Use the `addresses` field instead for better typing and IPv6 support") + public var ip(get, never):Int; + + private function get_ip():Int { + for (addr in this.addresses) { + switch (addr) { + case V4(ip): + return @:privateAccess ip.asNetworkOrderInt(); + case _: + } + } + + throw new UnsupportedFamilyException("This host does not have any associated IPv4 address"); + } + + /** + Creates a new `Host`. + + The name can be an IPv4 address (e.g. "127.0.0.1"), + or an IPv6 address (e.g. "::1"), in which case it will be parsed as such. + It can also be a hostname (e.g. "google.com"), + in which case the corresponding IP addresses are resolved using DNS. + + If the hostname could not be found, throws an exception. + **/ + public function new(name:String) { + this.host = name; + + final ipAddr = IpAddress.tryParse(name); + if (ipAddr != null) { + this.addresses = [ipAddr]; + return; + } + + final addresses = Dns.resolveSync(name); + if (addresses.length == 0) { + throw new DnsException("Could not resolve hostname " + name); + } + + this.addresses = addresses; + } + + private function getAddressesSorted(preference:FamilyPreference):Array { + final copy = this.addresses.copy(); + copy.sort((a, b) -> switch [preference, a, b] { + case [PreferIPv4, V4(_), V6(_)]: -1; + case [PreferIPv4, V6(_), V4(_)]: 1; + case [PreferIPv6, V6(_), V4(_)]: -1; + case [PreferIPv6, V4(_), V6(_)]: 1; + case _: 0; + }); + return copy; + } /** - Returns the IP representation of the host + Represents this host and all its IP addresses as a string. **/ - function toString():String; + public function toString():String { + final sb = new StringBuf(); + sb.addChar('"'.code); + sb.add(this.host); + sb.add("\" ("); + + final addresses = this.addresses; + if (addresses.length == 0) { + sb.add("no known addresses"); + } else { + for (i => addr in addresses) { + sb.add(addr.toString()); + if (i + 1 < addresses.length) { + sb.add(", "); + } + } + } + + sb.addChar(")".code); + return sb.toString(); + } /** Perform a reverse-DNS query to resolve a host name from an IP. **/ - function reverse():String; + @:deprecated("Use `sys.net.Dns.reverseSync` instead") + public function reverse():String { + final addresses = this.addresses; + if (addresses.length == 0) { + throw new Exception("There are no IP addresses associated with this host"); + } + + final address = addresses[0]; + final reversed = Dns.reverseSync(address); + if (reversed.length == 0) { + throw new DnsException('Reverse lookup failed for IP address $address'); + } + + return reversed[0]; + } /** - Returns the local computer host name + Returns the local computer hostname. **/ - static function localhost():String; + @:deprecated("Use `sys.net.Dns.getLocalHostname` instead") + public static function localhost():String { + return Dns.getLocalHostname(); + } +} + +@:noDoc @:noCompletion +private enum abstract FamilyPreference(Int) { + public var PreferIPv4 = 0; + public var PreferIPv6 = 1; } diff --git a/std/php/_std/sys/net/Host.hx b/std/sys/net/IpAddress.hx similarity index 58% rename from std/php/_std/sys/net/Host.hx rename to std/sys/net/IpAddress.hx index 9d6997b60ec..61ad05e574a 100644 --- a/std/php/_std/sys/net/Host.hx +++ b/std/sys/net/IpAddress.hx @@ -1,5 +1,5 @@ /* - * Copyright (C)2005-2019 Haxe Foundation + * Copyright (C)2005-2024 Haxe Foundation * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), @@ -22,41 +22,40 @@ package sys.net; -import php.Global.*; -import php.SuperGlobal.*; - -@:coreApi -class Host { - public var host(default, null):String; - - private var _ip:String; - - public var ip(default, null):Int; +/** + Represents an Internet Protocol (IP) address. +**/ +@:using(sys.net.IpAddress.IpAddressTools) +enum IpAddress { + V4(addr:Ipv4Address); + V6(addr:Ipv6Address); +} - public function new(name:String):Void { - host = name; - if (~/^(\d{1,3}\.){3}\d{1,3}$/.match(name)) { - _ip = name; - } else { - _ip = gethostbyname(name); - if (_ip == name) { - ip = 0; - return; - } +private final class IpAddressTools { + public static function toString(ip:IpAddress):String { + return switch (ip) { + case V4(addr): + addr.toString(); + case V6(addr): + addr.toString(); } - var p = _ip.split('.'); - ip = intval(sprintf('%02X%02X%02X%02X', p[3], p[2], p[1], p[0]), 16); } - public function toString():String { - return _ip; - } + /** + Tries to parse the given string as an IPv4 or an IPv6 address. + @param str The string to parse. + **/ + public static function tryParse(_:Enum, str:String):Null { + final ipv4 = Ipv4Address.tryParse(str); + if (ipv4 != null) { + return V4(ipv4); + } - public function reverse():String { - return gethostbyaddr(_ip); - } + final ipv6 = Ipv6Address.tryParse(str); + if (ipv6 != null) { + return V6(ipv6); + } - public static function localhost():String { - return php.Syntax.coalesce(_SERVER['HTTP_HOST'], "localhost"); + return null; } } diff --git a/std/sys/net/Ipv4Address.hx b/std/sys/net/Ipv4Address.hx new file mode 100644 index 00000000000..dbd59d2e324 --- /dev/null +++ b/std/sys/net/Ipv4Address.hx @@ -0,0 +1,217 @@ +/* + * Copyright (C)2005-2024 Haxe Foundation + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +package sys.net; + +import haxe.Int32; +import haxe.ds.Vector; +import sys.net.IpAddress; + +/** + An IPv4 address, as defined in IETF RFC 791. + This value is immutable. +**/ +@:notNull +abstract Ipv4Address(Ipv4AddressImpl) { + public static final BROADCAST:Ipv4Address = new Ipv4Address(255, 255, 255, 255); + public static final LOCALHOST:Ipv4Address = new Ipv4Address(127, 0, 0, 1); + public static final ANY:Ipv4Address = new Ipv4Address(0, 0, 0, 0); + + /** + Constructs a new IPv4 address from four octets. + **/ + public inline function new(a:Int, b:Int, c:Int, d:Int) { + this = Ipv4AddressImpl.fromOctets(a, b, c, d); + } + + /** + Returns true if two IPv4 addresses are equal. + **/ + @:op(A == B) + public static inline function equals(lhs:Ipv4Address, rhs:Ipv4Address):Bool { + return Ipv4AddressImpl.equals(cast lhs, cast rhs); + } + + /** + Returns true if two IPv4 addresses are not equal. + **/ + @:op(A != B) + public static inline function notEquals(lhs:Ipv4Address, rhs:Ipv4Address):Bool { + return !equals(lhs, rhs); + } + + /** + Returns true if this is a loopback address. + **/ + public inline function isLoopback():Bool { + return this.isLoopback(); + } + + /** + Returns true if this is a link-local address. + **/ + public inline function isLinkLocal():Bool { + return this.isLinkLocal(); + } + + /** + Returns true if this is a multicast address. + **/ + public inline function isMulticast():Bool { + return this.isMulticast(); + } + + /** + Returns true if this address is unspecified. + **/ + public inline function isUnspecified():Bool { + return abstract == Ipv4Address.ANY; + } + + /** + Returns true if this is a broadcast address. + **/ + public inline function isBroadcast():Bool { + return abstract == Ipv4Address.BROADCAST; + } + + @:to + private inline function asIpAddress():IpAddress { + return IpAddress.V4(abstract); + } + + /** + Returns this IPv4 address represented as a big-endian integer. + **/ + public inline function asNetworkOrderInt():Int32 { + return @:privateAccess this.repr; + } + + /** + Creates a new IPv4 address object from a big-endian integer. + **/ + public static inline function fromNetworkOrderInt(i:Int32):Ipv4Address { + return cast new Ipv4AddressImpl(i); + } + + /** + Gets a text, dotted-decimal representation of this IPv4 address. + **/ + public inline function toString():String { + return this.toString(); + } + + /** + Tries to parse a string as an IPv4 address. + + @param str The string to parse. It must be in the dotted decimal notation. + @return The parsed IPv4 address or `null` if the string could not be parsed. + **/ + public static inline function tryParse(str:String):Null { + return cast Ipv4AddressImpl.tryParse(str); + } +} + +@:noDoc +private final class Ipv4AddressImpl { + /** + Inner representation of the IPv4 address. + **/ + private final repr:Int32; + + public function new(repr:Int32) { + this.repr = repr; + } + + private function octets():Vector { + final repr = this.repr; + final v = new Vector(4); + v[0] = repr & 255; + v[1] = (repr >> 8) & 255; + v[2] = (repr >> 16) & 255; + v[3] = (repr >> 24) & 255; + return v; + } + + public inline function isLoopback():Bool { + final octets = this.octets(); + return octets[0] == 127; + } + + public inline function isLinkLocal():Bool { + final octets = this.octets(); + return octets[0] == 169 && octets[1] == 254; + } + + public inline function isMulticast():Bool { + final octets = this.octets(); + return octets[0] >= 224 && octets[0] <= 239; + } + + public function toString():String { + final octets = this.octets(); + final sb = new StringBuf(); + sb.add(octets[0]); + sb.addChar(".".code); + sb.add(octets[1]); + sb.addChar(".".code); + sb.add(octets[2]); + sb.addChar(".".code); + sb.add(octets[3]); + return sb.toString(); + } + + public static inline function equals(lhs:Ipv4AddressImpl, rhs:Ipv4AddressImpl):Bool { + return lhs.repr == rhs.repr; + } + + public static function fromOctets(a:Int, b:Int, c:Int, d:Int):Ipv4AddressImpl { + var value:Int32 = 0; + value |= (a & 255); + value |= (b & 255) << 8; + value |= (c & 255) << 16; + value |= (d & 255) << 24; + return new Ipv4AddressImpl(value); + } + + public static function tryParse(str:String):Null { + final parts = StringTools.trim(str).split("."); + if (parts.length != 4) { + return null; + } + + final octets:Array = []; + for (part in parts) { + static final patternDigit = ~/^\d{1,3}$/; + if (!patternDigit.match(part)) { + return null; + } + final octet = Std.parseInt(part); + if (octet > 255) { + return null; + } + octets.push(octet); + } + + return Ipv4AddressImpl.fromOctets(octets[0], octets[1], octets[2], octets[3]); + } +} diff --git a/std/sys/net/Ipv6Address.hx b/std/sys/net/Ipv6Address.hx new file mode 100644 index 00000000000..e7d6c15ee72 --- /dev/null +++ b/std/sys/net/Ipv6Address.hx @@ -0,0 +1,292 @@ +/* + * Copyright (C)2005-2024 Haxe Foundation + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +package sys.net; + +import haxe.ds.Vector; +import haxe.exceptions.ArgumentException; +import haxe.io.Bytes; +import haxe.io.BytesData; + +/** + An IPv6 address, as defined in IETF RFC 4291. + This value is immutable. +**/ +@:notNull +abstract Ipv6Address(Ipv6AddressImpl) { + public static final LOCALHOST:Ipv6Address = new Ipv6Address(0, 0, 0, 0, 0, 0, 0, 1); + public static final ANY:Ipv6Address = new Ipv6Address(0, 0, 0, 0, 0, 0, 0, 0); + + /** + Constructs a new IPv6 address from eight 16-bit groups. + **/ + public inline function new(a:Int, b:Int, c:Int, d:Int, e:Int, f:Int, g:Int, h:Int) { + this = Ipv6AddressImpl.fromGroups(a, b, c, d, e, f, g, h); + } + + /** + Returns true if two IPv6 addresses are equal. + **/ + @:op(A == B) + public static inline function equals(lhs:Ipv6Address, rhs:Ipv6Address):Bool { + return Ipv6AddressImpl.equals(cast lhs, cast rhs); + } + + /** + Returns true if two IPv6 addresses are not equal. + **/ + @:op(A != B) + public static inline function notEquals(lhs:Ipv6Address, rhs:Ipv6Address):Bool { + return !equals(lhs, rhs); + } + + /** + Returns true if this is a loopback address. + **/ + public inline function isLoopback():Bool { + return abstract == Ipv6Address.LOCALHOST; + } + + /** + Returns true if this IPv6 address is an IPv4-mapped address. + **/ + public inline function isIpv4Mapped():Bool { + return this.isIpv4Mapped(); + } + + /** + Returns a text, lowercase hexadecimal representation of this IPv6 address. + **/ + public inline function toString():String { + return this.toString(); + } + + @:to + private inline function asIpAddress():IpAddress { + return IpAddress.V6(abstract); + } + + /** + Returns this IPv6 address represented as big-endian bytes. + **/ + private inline function asNetworkOrderBytes():BytesData { + return @:privateAccess this.repr; + } + + /** + Creates a new IPv6 address object from big-endian bytes. + **/ + private static inline function fromNetworkOrderBytes(b:BytesData):Ipv6Address { + return cast new Ipv6AddressImpl(b); + } + + /** + Tries to parse a string as an IPv6 address. + + @param str The string to parse. + @return The parsed IPv6 address, or `null` if the string could not be parsed. + **/ + public static inline function tryParse(str:String):Null { + return cast Ipv6AddressImpl.tryParse(str); + } +} + +@:noDoc +private final class Ipv6AddressImpl { + /** + Inner representation of the IPv6 address. + **/ + private final repr:BytesData; + + public function new(repr:BytesData) { + this.repr = repr; + } + + private function groups():Vector { + final bytes = Bytes.ofData(this.repr); + final v = new Vector(8); + v[0] = bytes.getUInt16(14); + v[1] = bytes.getUInt16(12); + v[2] = bytes.getUInt16(10); + v[3] = bytes.getUInt16(8); + v[4] = bytes.getUInt16(6); + v[5] = bytes.getUInt16(4); + v[6] = bytes.getUInt16(0); + v[7] = bytes.getUInt16(2); + return v; + } + + public function isIpv4Mapped():Bool { + final groups = this.groups(); + if (groups[0] != 0) + return false; + if (groups[1] != 0) + return false; + if (groups[2] != 0) + return false; + if (groups[3] != 0) + return false; + if (groups[4] != 0) + return false; + return groups[5] == 0xffff; + } + + public function toString():String { + final groups = this.groups(); + + // Detect longest run of zeros + var firstZeroAt:Null = null; + var zerosCount:Int = 0; + { + var currentStart:Null = null; + var currentLen:Int = 0; + for (i in 0...8) { + if (groups[i] == 0) { + if (currentStart == null) { + currentStart = i; + } + currentLen += 1; + if (currentLen > zerosCount) { + zerosCount = currentLen; + firstZeroAt = currentStart; + } + } else { + currentStart = null; + currentLen = 0; + } + } + } + + final sb = new StringBuf(); + + if (zerosCount >= 2) { + for (i in 0...firstZeroAt) { + sb.add(StringTools.hex(groups[i])); + if (i + 1 < firstZeroAt) { + sb.addChar(":".code); + } + } + sb.add("::"); + for (j in (firstZeroAt + zerosCount)...8) { + sb.add(StringTools.hex(groups[j])); + if (j < 7) { + sb.addChar(":".code); + } + } + } else { + for (i in 0...8) { + sb.add(StringTools.hex(groups[i])); + if (i < 7) { + sb.addChar(":".code); + } + } + } + + return sb.toString().toLowerCase(); + } + + public static function equals(lhs:Ipv6AddressImpl, rhs:Ipv6AddressImpl):Bool { + return Bytes.ofData(lhs.repr).compare(Bytes.ofData(rhs.repr)) == 0; + } + + public static function fromGroups(a:Int, b:Int, c:Int, d:Int, e:Int, f:Int, g:Int, h:Int):Ipv6AddressImpl { + final bytes = Bytes.alloc(16); + bytes.setUInt16(14, a); + bytes.setUInt16(12, b); + bytes.setUInt16(10, c); + bytes.setUInt16(8, d); + bytes.setUInt16(6, e); + bytes.setUInt16(4, f); + bytes.setUInt16(0, g); + bytes.setUInt16(2, h); + return new Ipv6AddressImpl(bytes.getData()); + } + + public static function fromGroupsVector(groups:Vector):Ipv6AddressImpl { + if (groups.length != 8) { + throw new ArgumentException("Groups do not represent an IPv6 address"); + } + + final a = groups[0]; + final b = groups[1]; + final c = groups[2]; + final d = groups[3]; + final e = groups[4]; + final f = groups[5]; + final g = groups[6]; + final h = groups[7]; + + return Ipv6AddressImpl.fromGroups(a, b, c, d, e, f, g, h); + } + + public static function tryParse(str:String):Null { + static final patternHex = ~/^[0-9a-f]{1,4}$/; + + final parts = StringTools.trim(str).toLowerCase().split(":"); + if (parts.length < 3 || parts.length > 8) { + return null; + } + + final groups = new Vector(8); + groups.fill(0); + + var i:Int = 0; + while (i < 8) { + if (i >= parts.length) { + return null; + } + + final part = parts[i]; + if (part == "") { + break; + } + + if (!patternHex.match(part)) { + return null; + } + + groups[i] = Std.parseInt('0x$part'); + i++; + } + + var j:Int = 0; + while ((parts.length - 1 - j) > i) { + final part = parts[(parts.length - 1 - j)]; + if (part == "") { + if ((i == 0 && j == 0) || (i + j + 2 == parts.length)) { + break; + } else { + return null; + } + } + + if (!patternHex.match(part)) { + return null; + } + + groups[7 - j] = Std.parseInt('0x$part'); + j++; + } + + return Ipv6AddressImpl.fromGroupsVector(groups); + } +} diff --git a/std/eval/_std/sys/net/Host.hx b/std/sys/net/UnsupportedFamilyException.hx similarity index 65% rename from std/eval/_std/sys/net/Host.hx rename to std/sys/net/UnsupportedFamilyException.hx index cbc1adcd305..720e8af2f89 100644 --- a/std/eval/_std/sys/net/Host.hx +++ b/std/sys/net/UnsupportedFamilyException.hx @@ -1,5 +1,5 @@ /* - * Copyright (C)2005-2019 Haxe Foundation + * Copyright (C)2005-2024 Haxe Foundation * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), @@ -22,32 +22,18 @@ package sys.net; -class Host { - public var host(default, null):String; - public var ip(default, null):Int; - - public function new(name:String) { - host = name; - init(resolve(name)); - } - - public function toString() { - return hostToString(ip); +import haxe.Exception; +import haxe.PosInfos; +import haxe.exceptions.NotImplementedException; + +/** + This exception is thrown when an operation does not support requested address family. +**/ +class UnsupportedFamilyException extends NotImplementedException { + /** + Creates a new instance of `UnsupportedFamilyException`. + **/ + public function new(message:String, ?previous:Exception, ?pos:PosInfos) { + super(message, previous, pos); } - - public function reverse() { - return hostReverse(ip); - } - - function init(ip:Int) { - this.ip = ip; - } - - extern static public function localhost():String; - - extern static function hostReverse(ip:Int):String; - - extern static function hostToString(ip:Int):String; - - extern static function resolve(name:String):Int; } diff --git a/tests/sys/src/Main.hx b/tests/sys/src/Main.hx index 95236e421d1..0970f05d6c6 100644 --- a/tests/sys/src/Main.hx +++ b/tests/sys/src/Main.hx @@ -10,6 +10,8 @@ class Main { runner.addCase(new TestFileSystem()); runner.addCase(new io.TestFile()); runner.addCase(new io.TestFileInput()); + runner.addCase(new net.TestIpv4Address()); + runner.addCase(new net.TestIpv6Address()); #if !js runner.addCase(new io.TestProcess()); #end diff --git a/tests/sys/src/net/TestIpv4Address.hx b/tests/sys/src/net/TestIpv4Address.hx new file mode 100644 index 00000000000..b26078c0429 --- /dev/null +++ b/tests/sys/src/net/TestIpv4Address.hx @@ -0,0 +1,60 @@ +package net; + +import sys.net.Ipv4Address; +import utest.Assert; +import utest.Test; + +class TestIpv4Address extends Test { + public function testAddressesAreComparedByValue() { + Assert.isTrue(new Ipv4Address(127, 0, 0, 1) == Ipv4Address.LOCALHOST); + Assert.isTrue(new Ipv4Address(1, 2, 3, 4) == new Ipv4Address(1, 2, 3, 4)); + Assert.isFalse(new Ipv4Address(1, 2, 3, 4) == new Ipv4Address(5, 6, 7, 8)); + Assert.isTrue(new Ipv4Address(1, 2, 3, 4) != new Ipv4Address(5, 6, 7, 8)); + } + + public function testAddressesAreCorrectlyStringified() { + Assert.equals("127.0.0.1", Ipv4Address.LOCALHOST.toString()); + Assert.equals("1.2.3.4", new Ipv4Address(1, 2, 3, 4).toString()); + } + + public function testValidAddressesAreSuccessfullyParsed() { + Assert.isTrue(new Ipv4Address(0, 0, 0, 0) == Ipv4Address.tryParse("0.0.0.0")); + Assert.isTrue(new Ipv4Address(1, 2, 3, 4) == Ipv4Address.tryParse("1.2.3.4")); + Assert.isTrue(new Ipv4Address(1, 1, 1, 1) == Ipv4Address.tryParse(" 1.1.1.1 ")); + Assert.isTrue(new Ipv4Address(255, 255, 255, 255) == Ipv4Address.tryParse("255.255.255.255")); + } + + public function testInvalidAddressesAreNotParsed() { + Assert.isNull(Ipv4Address.tryParse("0")); + Assert.isNull(Ipv4Address.tryParse("0.0")); + Assert.isNull(Ipv4Address.tryParse("0.0.0")); + Assert.isNull(Ipv4Address.tryParse("0.0.0.0.0")); + Assert.isNull(Ipv4Address.tryParse("-1.-2.-3.-4")); + Assert.isNull(Ipv4Address.tryParse("256.256.256.256")); + + // No name resolution + Assert.isNull(Ipv4Address.tryParse("localhost")); + + // No IPv6 + Assert.isNull(Ipv4Address.tryParse("::1")); + + // No masks + Assert.isNull(Ipv4Address.tryParse("192.180.5.0/24")); + + // No alternative notations + Assert.isNull(Ipv4Address.tryParse("2147483775")); + Assert.isNull(Ipv4Address.tryParse("00110110101010101010111111010010")); + Assert.isNull(Ipv4Address.tryParse("00110110 10101010 10101111 11010010")); + Assert.isNull(Ipv4Address.tryParse("af.c0.5.b")); + Assert.isNull(Ipv4Address.tryParse("c56701a3")); + Assert.isNull(Ipv4Address.tryParse("0xc56701a3")); + } + + public function testAddressesAreConvertedToNetworkOrder() { + Assert.equals(0x0100007f, Ipv4Address.LOCALHOST.asNetworkOrderInt()); + } + + public function testAddressesAreConvertedFromNetworkOrder() { + Assert.equals("127.0.0.1", Ipv4Address.fromNetworkOrderInt(0x0100007f).toString()); + } +} diff --git a/tests/sys/src/net/TestIpv6Address.hx b/tests/sys/src/net/TestIpv6Address.hx new file mode 100644 index 00000000000..8d0cbbfb3c6 --- /dev/null +++ b/tests/sys/src/net/TestIpv6Address.hx @@ -0,0 +1,58 @@ +package net; + +import sys.net.Ipv6Address; +import utest.Assert; +import utest.Test; + +class TestIpv6Address extends Test { + public function testAddressesAreComparedByValue() { + Assert.isTrue(new Ipv6Address(0, 0, 0, 0, 0, 0, 0, 1) == Ipv6Address.LOCALHOST); + Assert.isTrue(new Ipv6Address(1, 2, 3, 4, 5, 6, 7, 8) == new Ipv6Address(1, 2, 3, 4, 5, 6, 7, 8)); + Assert.isFalse(new Ipv6Address(1, 2, 3, 4, 5, 6, 7, 8) == new Ipv6Address(5, 6, 7, 8, 1, 2, 3, 4)); + Assert.isTrue(new Ipv6Address(1, 2, 3, 4, 5, 6, 7, 8) != new Ipv6Address(5, 6, 7, 8, 1, 2, 3, 4)); + } + + public function testAddressesAreCorrectlyStringified() { + Assert.equals("::1", Ipv6Address.LOCALHOST.toString()); + Assert.equals("::", Ipv6Address.ANY.toString()); + Assert.equals("1:2:3:4:5:6:7:8", new Ipv6Address(1, 2, 3, 4, 5, 6, 7, 8).toString()); + Assert.equals("1:0:2:0:3:0:4:0", new Ipv6Address(1, 0, 2, 0, 3, 0, 4, 0).toString()); + Assert.equals("fe80::1:2", new Ipv6Address(0xfe80, 0, 0, 0, 0, 0, 0x1, 0x2).toString()); + Assert.equals("fe80::", new Ipv6Address(0xfe80, 0, 0, 0, 0, 0, 0, 0).toString()); + Assert.equals("10:0:0:20::30", new Ipv6Address(0x10, 0, 0, 0x20, 0, 0, 0, 0x30).toString()); + Assert.equals("10::20:0:0:30", new Ipv6Address(0x10, 0, 0, 0, 0x20, 0, 0, 0x30).toString()); + } + + public function testValidAddressesAreSuccessfullyParsed() { + Assert.isTrue(new Ipv6Address(1, 2, 3, 4, 5, 6, 7, 8) == Ipv6Address.tryParse("1:2:3:4:5:6:7:8")); + Assert.isTrue(new Ipv6Address(1, 2, 3, 4, 5, 6, 7, 8) == Ipv6Address.tryParse("01:02:03:04:05:06:07:08")); + Assert.isTrue(new Ipv6Address(0, 0, 0, 0, 0, 0, 0, 1) == Ipv6Address.tryParse("::1")); + Assert.isTrue(new Ipv6Address(0, 0, 0, 0, 0, 0, 0, 0) == Ipv6Address.tryParse("::")); + Assert.isTrue(new Ipv6Address(1, 0, 2, 0, 0, 0, 3, 4) == Ipv6Address.tryParse("1:0:2::3:4")); + Assert.isTrue(new Ipv6Address(0, 0, 0, 0, 0, 0, 0xab, 0xcd) == Ipv6Address.tryParse("::AB:CD")); + Assert.isTrue(new Ipv6Address(0xf2, 0, 0, 0, 0, 0, 0, 0) == Ipv6Address.tryParse(" f2:: ")); + Assert.isTrue(new Ipv6Address(0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, + 0xffff) == Ipv6Address.tryParse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")); + } + + public function testInvalidAddressesAreNotParsed() { + Assert.isNull(Ipv6Address.tryParse("1:2:3:4:5:6:7")); + Assert.isNull(Ipv6Address.tryParse("1:2:3:4:5:6:7:8:9")); + Assert.isNull(Ipv6Address.tryParse("1::2::3")); + Assert.isNull(Ipv6Address.tryParse("0ffff::1")); + Assert.isNull(Ipv6Address.tryParse("fffff::1")); + Assert.isNull(Ipv6Address.tryParse("::-1")); + + // No name resolution + Assert.isNull(Ipv6Address.tryParse("localhost")); + + // No IPv4 + Assert.isNull(Ipv6Address.tryParse("127.0.0.1")); + + // No masks + Assert.isNull(Ipv6Address.tryParse("1:2:3:4:5:6:0:0/96")); + + // No dual addresses + Assert.isNull(Ipv6Address.tryParse("::ffff:1.2.3.4")); + } +} diff --git a/tests/sys/src/net/TestSocket.hx b/tests/sys/src/net/TestSocket.hx index 224d090ffbf..c96f1955423 100644 --- a/tests/sys/src/net/TestSocket.hx +++ b/tests/sys/src/net/TestSocket.hx @@ -1,6 +1,7 @@ package net; -import sys.net.*; +import sys.net.Host; +import sys.net.Socket; import utest.Assert; class TestSocket extends utest.Test { @@ -12,8 +13,9 @@ class TestSocket extends utest.Test { } public function teardown() { - for(socket in registeredSockets) { - if(socket == null) continue; + for (socket in registeredSockets) { + if (socket == null) + continue; socket.close(); } registeredSockets = [];