From cd0c6873077fc90d6240fb46d014c59443650a95 Mon Sep 17 00:00:00 2001 From: Bram Date: Tue, 9 Apr 2024 20:38:35 +0200 Subject: [PATCH] Add server name module --- src/Internal/Config/Text.elm | 2 +- src/Internal/Grammar/ServerName.elm | 136 ++++++++++++++++++++-------- src/Internal/Grammar/UserId.elm | 30 ++++-- tests/Test/Grammar/ServerName.elm | 127 ++++++++++++++++++++++++++ 4 files changed, 250 insertions(+), 45 deletions(-) create mode 100644 tests/Test/Grammar/ServerName.elm diff --git a/src/Internal/Config/Text.elm b/src/Internal/Config/Text.elm index a0209a5..596aa01 100644 --- a/src/Internal/Config/Text.elm +++ b/src/Internal/Config/Text.elm @@ -349,7 +349,7 @@ leakingValueFound leaking_value = parses : - { reservedIPs : + { reservedIPs : { ipv6Toipv4 : String , multicast : String , futureUse : String diff --git a/src/Internal/Grammar/ServerName.elm b/src/Internal/Grammar/ServerName.elm index 8018617..1ee3cff 100644 --- a/src/Internal/Grammar/ServerName.elm +++ b/src/Internal/Grammar/ServerName.elm @@ -1,13 +1,29 @@ -module Internal.Grammar.ServerName exposing (..) -{-| A homeserver is uniquely identified by its server name. The server name +module Internal.Grammar.ServerName exposing + ( ServerName, toString, fromString + , servernameParser + ) + +{-| + + +# Server name + +A homeserver is uniquely identified by its server name. The server name represents the address at which the homeserver in question can be reached by other homeservers. + +@docs ServerName, toString, fromString + + +## Parser + +@docs serverNameParser + -} import Internal.Tools.ParserExtra as PE -import Parser as P exposing (Parser, (|.), (|=)) -import Internal.Config.Log exposing (Log, log) -import Internal.Config.Text as Text +import Parser as P exposing ((|.), (|=), Parser) + {-| The hostname is the location where the server can be found. @@ -15,19 +31,25 @@ Notice how the Matrix spec specifies that the hostname can either be a DNS name, an IPv4Address or an IPv6Address. Since the IPv4Address is compatible with the specification of DNS names, however, and RFC1123 (section 2.1) does not require a client to distinguish them, we treat IPv4Addresses like DNS names. + -} type HostName = DNS String | IPv6 IPv6Address + {-| The IPv6Address is represented by a list of items BEFORE and AFTER the double colons (::). -} -type alias IPv6Address = { front : List String, back : List String } +type alias IPv6Address = + { front : List String, back : List String } + {-| The server name is a combination of a hostname and an optional port. -} -type ServerName = ServerName { host : HostName, port_ : Maybe Int } +type ServerName + = ServerName { host : HostName, port_ : Maybe Int } + {-| Parser for the DNS name record. The Matrix spec bases its grammar on the standard for internet host names, as specified by RFC1123, section 2.1, with an @@ -43,7 +65,7 @@ extension IPv6 literals. Host software MUST handle host names of up to 63 characters and SHOULD handle host names of up to 255 characters. - + [RFC-952 §Assumptions-1] A "name" (Net, Host, Gateway, or Domain name) is a text string up @@ -52,6 +74,7 @@ extension IPv6 literals. they serve to delimit components of "domain style names". (See RFC-921, "Domain Name System Implementation Schedule", for background). + -} dnsNameParser : Parser String dnsNameParser = @@ -59,6 +82,23 @@ dnsNameParser = |. P.chompWhile (\c -> Char.isAlphaNum c || c == '-' || c == '.') |> P.getChompedString + +fromString : String -> Maybe ServerName +fromString s = + P.run (servernameParser |. P.end) s + |> (\out -> + case out of + Ok _ -> + out + + Err e -> + Debug.log "No parse" e + |> always (Debug.log "original" s) + |> always out + ) + |> Result.toMaybe + + {-| Parse a Hostname. -} hostnameParser : Parser HostName @@ -72,6 +112,7 @@ hostnameParser = |= dnsNameParser ] + {-| Parse all values to the left of the double colon (::) -} ipv6LeftParser : Parser (List String) @@ -82,12 +123,13 @@ ipv6LeftParser = , P.succeed (|>) |= PE.times 1 7 (ipv6NumParser |. P.symbol ":") |= P.oneOf - [ P.succeed (\bottom tail -> tail ++ [bottom]) + [ P.succeed (\bottom tail -> tail ++ [ bottom ]) |= ipv6NumParser , P.succeed identity ] ] + {-| Parse an ordinary IPv6 number -} ipv6NumParser : Parser String @@ -97,6 +139,7 @@ ipv6NumParser = |> PE.times 1 4 |> P.map String.concat + {-| Parse an IPv6 Address -} ipv6Parser : Parser IPv6Address @@ -108,37 +151,47 @@ ipv6Parser = |= ipv6RightParser (8 - List.length front) ) + {-| Parse all values to the right of the double colon (::) -} ipv6RightParser : Int -> Parser (List String) ipv6RightParser n = - P.succeed identity - |. P.symbol ":" - |= P.oneOf - [ P.succeed (::) - |= ipv6NumParser - |= PE.times 1 (n - 1) - ( P.succeed identity - |. P.symbol ":" - |= ipv6NumParser - ) - , P.succeed [] - ] + if n > 0 then + P.succeed identity + |. P.symbol ":" + |= P.oneOf + [ P.succeed (::) + |= ipv6NumParser + |= PE.times 0 + (n - 1) + (P.succeed identity + |. P.symbol ":" + |= ipv6NumParser + ) + , P.succeed [] + ] + + else + P.succeed [] + {-| Convert an IPv6 address to a readable string format -} ipv6ToString : IPv6Address -> String ipv6ToString { front, back } = - ( if List.length front == 8 then + (if List.length front == 8 then front - else if List.length back == 8 then + + else if List.length back == 8 then back - else - List.concat [ front, [""], back ] + + else + List.concat [ front, [ "" ], back ] ) |> List.intersperse ":" |> String.concat + portParser : Parser Int portParser = P.chompIf Char.isDigit @@ -148,18 +201,20 @@ portParser = (\v -> case String.toInt v of Just i -> - if 0 <= i && i <= 2^16 - 1 then + if 0 <= i && i <= 2 ^ 16 - 1 then P.succeed i + else P.problem ("Port out of range: " ++ v) - + Nothing -> P.problem "Not a port number" ) + servernameParser : Parser ServerName servernameParser = - P.succeed (\h p -> ServerName { host = h, port_ = p } ) + P.succeed (\h p -> ServerName { host = h, port_ = p }) |= hostnameParser |= P.oneOf [ P.succeed Just @@ -168,6 +223,7 @@ servernameParser = , P.succeed Nothing ] + toString : ServerName -> String toString (ServerName { host, port_ }) = let @@ -176,17 +232,23 @@ toString (ServerName { host, port_ }) = case host of DNS name -> name - + IPv6 { front, back } -> - ( if List.length front == 8 then - front - else if List.length back == 8 then - back - else - List.concat [ front, [""], back ] + (if List.length front == 8 then + List.intersperse ":" front + + else if List.length back == 8 then + List.intersperse ":" back + + else + List.concat + [ List.intersperse ":" front + , [ "::" ] + , List.intersperse ":" back + ] ) - |> List.intersperse ":" |> String.concat + |> (\i -> "[" ++ i ++ "]") portString : String portString = @@ -195,4 +257,4 @@ toString (ServerName { host, port_ }) = |> Maybe.map ((++) ":") |> Maybe.withDefault "" in - hostString ++ portString + hostString ++ portString diff --git a/src/Internal/Grammar/UserId.elm b/src/Internal/Grammar/UserId.elm index d56ea9a..be433ba 100644 --- a/src/Internal/Grammar/UserId.elm +++ b/src/Internal/Grammar/UserId.elm @@ -1,5 +1,9 @@ module Internal.Grammar.UserId exposing (..) -{-| # User ids + +{-| + + +# User ids Users within Matrix are uniquely identified by their Matrix user ID. The user ID is namespaced to the homeserver which allocated the account and has the form: @@ -7,7 +11,7 @@ ID is namespaced to the homeserver which allocated the account and has the form: @localpart:domain The localpart of a user ID is an opaque identifier for that user. It MUST NOT -be empty, and MUST contain only the characters a-z, 0-9, ., _, =, -, /, and +. +be empty, and MUST contain only the characters a-z, 0-9, ., \_, =, -, /, and +. The domain of a user ID is the server name of the homeserver which allocated the account. @@ -36,9 +40,17 @@ localparts from the expanded character set: import Internal.Grammar.ServerName as ServerName exposing (ServerName) import Internal.Tools.ParserExtra as PE -import Parser as P exposing (Parser, (|.), (|=)) +import Parser as P exposing ((|.), (|=), Parser) + + +type UserID + = UserID { localpart : String, domain : ServerName } + + +fromString : String -> Maybe UserID +fromString = + P.run userIdParser >> Result.toMaybe -type UserID = UserID { localpart : String, domain : ServerName } localpartParser : Parser String localpartParser = @@ -47,22 +59,26 @@ localpartParser = |> PE.times 1 255 |> P.map String.concat + toString : UserID -> String toString (UserID { localpart, domain }) = String.concat [ "@", localpart, ":", ServerName.toString domain ] + userIdParser : Parser UserID userIdParser = - P.succeed (\l d -> UserID { localpart = l, domain = d } ) + P.succeed (\l d -> UserID { localpart = l, domain = d }) |. P.symbol "@" |= localpartParser |. P.symbol ":" |= ServerName.servernameParser + validHistoricalUsernameChar : Char -> Bool validHistoricalUsernameChar c = let i : Int - i = Char.toCode c + i = + Char.toCode c in - (0x21 <= i && i <= 0x39) || (0x3B <= i && i <= 0x7E) + (0x21 <= i && i <= 0x39) || (0x3B <= i && i <= 0x7E) diff --git a/tests/Test/Grammar/ServerName.elm b/tests/Test/Grammar/ServerName.elm new file mode 100644 index 0000000..02ac97a --- /dev/null +++ b/tests/Test/Grammar/ServerName.elm @@ -0,0 +1,127 @@ +module Test.Grammar.ServerName exposing (..) + +import Expect +import Fuzz exposing (Fuzzer) +import Internal.Grammar.ServerName as SN +import Test exposing (..) + + +dnsFuzzer : Fuzzer String +dnsFuzzer = + Fuzz.map2 + (\head tail -> + String.fromList (head :: tail) + ) + ("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + |> String.toList + |> Fuzz.oneOfValues + ) + ("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-." + |> String.toList + |> Fuzz.oneOfValues + |> Fuzz.listOfLengthBetween 0 (255 - 1) + ) + + +hostnameFuzzer : Fuzzer String +hostnameFuzzer = + Fuzz.oneOf + [ dnsFuzzer + , ipv4Fuzzer + , Fuzz.map (\x -> "[" ++ x ++ "]") ipv6Fuzzer + ] + + +ipv4Fuzzer : Fuzzer String +ipv4Fuzzer = + Fuzz.intRange 0 255 + |> Fuzz.listOfLength 4 + |> Fuzz.map + (List.map String.fromInt + >> List.intersperse "." + >> String.concat + ) + + +ipv6Fuzzer : Fuzzer String +ipv6Fuzzer = + let + num : Fuzzer String + num = + "0123456789abcdefABCDEF" + |> String.toList + |> Fuzz.oneOfValues + |> Fuzz.listOfLength 4 + |> Fuzz.map String.fromList + in + Fuzz.oneOf + [ Fuzz.listOfLength 8 num + |> Fuzz.map (List.intersperse ":") + |> Fuzz.map String.concat + , Fuzz.listOfLengthBetween 0 7 num + |> Fuzz.andThen + (\front -> + num + |> Fuzz.listOfLengthBetween 0 (8 - List.length front) + |> Fuzz.map + (\back -> + [ front + |> List.intersperse ":" + , [ "::" ] + , back + |> List.intersperse ":" + ] + |> List.concat + |> String.concat + ) + ) + ] + + +portFuzzer : Fuzzer String +portFuzzer = + Fuzz.oneOf + [ Fuzz.constant "" + , Fuzz.intRange 0 65535 + |> Fuzz.map (\p -> ":" ++ String.fromInt p) + ] + + +serverNameFuzzer : Fuzzer String +serverNameFuzzer = + Fuzz.map2 (++) hostnameFuzzer portFuzzer + + +suite : Test +suite = + describe "Server name tests" + [ describe "Checking correct values" + [ fuzz serverNameFuzzer + "Correct server names validate" + (\server -> + SN.fromString server + |> Maybe.map SN.toString + |> Maybe.map (String.replace "::" ":") + |> Expect.equal (Just <| String.replace "::" ":" server) + ) + , test "Checking spec examples" + (\() -> + let + examples : List String + examples = + [ "matrix.org" + , "matrix.org:8888" + , "1.2.3.4" + , "1.2.3.4:1234" + , "[1234:5678::abcd]" + , "[1234:5678::abcd]:5678" + ] + in + examples + |> List.map SN.fromString + |> List.map ((/=) Nothing) + |> Expect.equalLists + (List.repeat (List.length examples) True) + ) + ] + ]