Source file addressing.ml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
let ( let* ) = Result.bind
type t = IPv4 of string * int | IPv6 of string * int * int * int
type resolved = { address : t; unresolved_host : string }
type scheme = Bolt | Bolt_secure | Bolt_self_signed | Neo4j | Neo4j_secure | Neo4j_self_signed
type uri = { scheme : scheme; host : string; port : int; routing_context : (string * string) list }
let default_host = "localhost"
let default_port = 7687
let default_routing_targets = [ ":"; ":17601"; ":17687" ]
let config_error fmt =
Printf.ksprintf (fun message -> Error (Errors.Configuration_error message)) fmt
let host = function IPv4 (host, _) | IPv6 (host, _, _, _) -> host
let port = function IPv4 (_, port) | IPv6 (_, port, _, _) -> port
let to_string = function
| IPv4 (host, port) -> Printf.sprintf "%s:%d" host port
| IPv6 (host, port, _, _) -> Printf.sprintf "[%s]:%d" host port
let parse_port ~default_port port =
if port = "" then Ok default_port
else
match int_of_string_opt port with
| Some port when port >= 0 -> Ok port
| _ -> config_error "Invalid port %S" port
let split_host_port s =
match String.rindex_opt s ':' with
| Some i ->
let host = String.sub s 0 i in
let port = String.sub s (i + 1) (String.length s - i - 1) in
(host, port)
| None -> (s, "")
let parse_ipv4 ?(default_host = default_host) ?(default_port = default_port) s =
let raw_host, raw_port = split_host_port s in
let host = if raw_host = "" then default_host else raw_host in
let* port = parse_port ~default_port raw_port in
Ok (IPv4 (host, port))
let parse_ipv6 ?(default_host = default_host) ?(default_port = default_port) s =
let rest = String.sub s 1 (String.length s - 1) in
match String.index_opt rest ']' with
| None -> config_error "Invalid IPv6 address %S" s
| Some close ->
let host = String.sub rest 0 close in
let after = String.trim (String.sub rest (close + 1) (String.length rest - close - 1)) in
let host = if host = "" then default_host else host in
let* port =
if after = "" then Ok default_port
else if String.starts_with ~prefix:":" after then
parse_port ~default_port (String.sub after 1 (String.length after - 1))
else config_error "Invalid IPv6 address %S" s
in
Ok (IPv6 (host, port, 0, 0))
let parse ?(default_host = default_host) ?(default_port = default_port) s =
if String.starts_with ~prefix:"[" s then parse_ipv6 ~default_host ~default_port s
else parse_ipv4 ~default_host ~default_port s
let parse_list ?(default_host = default_host) ?(default_port = default_port) targets =
let rec go = function
| [] -> Ok []
| target :: rest ->
let* address = parse ~default_host ~default_port target in
let* addresses = go rest in
Ok (address :: addresses)
in
targets |> List.concat_map (String.split_on_char ' ') |> List.filter (fun s -> s <> "") |> go
let make_resolved address unresolved_host = { address; unresolved_host }
let resolved_host_name resolved = resolved.unresolved_host
let unresolved resolved =
match resolved.address with
| IPv4 (_, port) -> IPv4 (resolved.unresolved_host, port)
| IPv6 (_, port, _, _) -> IPv6 (resolved.unresolved_host, port, 0, 0)
let connect_failure_message ~address ~resolved ~(failures : Errors.failures) =
let address_strs = String.concat ", " resolved in
match failures.all with
| [] -> Printf.sprintf "Couldn't connect to %s (resolved to %s)" (to_string address) address_strs
| errors ->
let error_strs = String.concat "\n" (List.map Errors.to_string errors) in
Printf.sprintf "Couldn't connect to %s (resolved to %s):\n%s" (to_string address) address_strs
error_strs
let scheme_to_string = function
| Bolt -> "bolt"
| Bolt_secure -> "bolt+s"
| Bolt_self_signed -> "bolt+ssc"
| Neo4j -> "neo4j"
| Neo4j_secure -> "neo4j+s"
| Neo4j_self_signed -> "neo4j+ssc"
let supported_schemes = [ "bolt"; "bolt+s"; "bolt+ssc"; "neo4j"; "neo4j+s"; "neo4j+ssc" ]
let scheme_of_string = function
| "bolt" -> Some Bolt
| "bolt+s" -> Some Bolt_secure
| "bolt+ssc" -> Some Bolt_self_signed
| "neo4j" -> Some Neo4j
| "neo4j+s" -> Some Neo4j_secure
| "neo4j+ssc" -> Some Neo4j_self_signed
| _ -> None
let url_decode s = Uri.pct_decode (String.map (fun c -> if c = '+' then ' ' else c) s)
let parse_routing_context query =
String.split_on_char '&' query
|> List.fold_left
(fun acc pair ->
let* context = acc in
match String.index_opt pair '=' with
| None -> config_error "Invalid parameter %S in query string '%s'" pair query
| Some i ->
let key = String.sub pair 0 i in
let value = String.sub pair (i + 1) (String.length pair - i - 1) in
let key = url_decode key in
let value = url_decode value in
if value = "" then
config_error "Invalid parameter '%s=%s' in query string '%s'" key value query
else if List.mem_assoc key context then
config_error "Duplicated query parameter '%s' in query string '%s'" key query
else Ok ((key, value) :: context))
(Ok [])
|> Result.map List.rev
let parse_uri uri_string =
let parsed = Uri.of_string (String.trim uri_string) in
let* scheme =
match Uri.scheme parsed with
| None -> config_error "URI %S is missing a scheme" uri_string
| Some scheme_str -> (
match scheme_of_string (String.lowercase_ascii scheme_str) with
| Some scheme -> Ok scheme
| None ->
config_error "URI scheme %S is not supported. Supported URI schemes are %s" scheme_str
(String.concat ", " supported_schemes))
in
let* () =
match Uri.userinfo parsed with
| None -> Ok ()
| Some _ -> config_error "Username and password are not supported in the URI %S" uri_string
in
let host = match Uri.host parsed with Some h when h <> "" -> h | _ -> default_host in
let port = match Uri.port parsed with Some p -> p | None -> default_port in
let* routing_context =
match Uri.verbatim_query parsed with
| None | Some "" -> Ok []
| Some query -> parse_routing_context query
in
Ok { scheme; host; port; routing_context }