talircd/lib/server/connection.ml

732 lines
22 KiB
OCaml

open! Import
open Result_syntax
type membership = Router.membership
include (val Logging.sublogs logger "Connection")
let _todo_validation_please x = x
type t = {
router : Router.t;
server_info : Server_info.t;
addr : sockaddr;
outbox : Outbox.t;
mutable user : User.t option;
mutable pending_nick : name option;
mutable pending_userinfo : userinfo option;
}
let make ~router ~server_info ~addr = {
router;
server_info;
addr;
outbox = Outbox.make ();
user = None;
pending_nick = None;
pending_userinfo = None;
}
let outbox t = t.outbox
(* numeric replies *)
type reply = string * string list
type 'a result = ('a, reply) Result.t
let ( >>= ) = Result.bind
let list_of_errors = function
| Ok () -> []
| Error e -> [e]
let reply t (num, params) =
let prefix = Server_info.prefix t.server_info in
let target =
match t.user with
| Some me -> User.nick me
| None -> "*"
in
let always_trailing = match num with
| "301" | "332" | "353" -> true
| _ -> false
in
Outbox.send t.outbox
(Msg.make num (target :: params)
~prefix ~always_trailing)
let tryagain cmd = "263", [cmd; "Please wait a while and try again."]
let away nick text = "301", [nick; text]
let nosuchnick tgt = "401", [tgt; "No such nick/channel"]
let nosuchchannel tgt = "403", [tgt; "No such channel"]
let cannotsendtochan tgt = "404", [tgt; "Cannot send to channel"]
let norecipient = "411", ["No recipient given (PRIVMSG)"]
let notexttosend = "412", ["No text to send"]
let unknowncommand cmd = "421", [cmd; "Unknown command"]
let nonicknamegiven = "431", ["No nickname given"]
let erroneusnickname nick = "432", [nick; "Erroneus nickname"]
let nicknameinuse nick = "433", [nick; "Nickname is already in use"]
let usernotinchannel n c = "442", [n; c; "They aren't on that channel"]
let notonchannel chan = "442", [chan; "You're not on that channel"]
let notregistered = "451", ["You have not registered"]
let needmoreparams cmd = "461", [cmd; "Not enough parameters"]
let alreadyregistered = "462", ["Unauthorized command (already registered)"]
let channelisfull chan = "471", [chan; "Cannot join channel (+l)"]
let unknownmode chr = "472", [String.make 1 chr; "is an unknown mode char to me"]
let chanoprivsneeded chan = "482", [chan; "You're not channel operator"]
let umodeunknownflag = "501", ["Unknown MODE flag"]
let usersdontmatch_set = "502", ["Can't change mode for other users"]
let usersdontmatch_get = "502", ["Can't view mode for other users"]
(* permission checking *)
let require_registered t : User.t result =
match t.user with
| Some me -> Ok me
| None -> Error notregistered
let require_same_user user me =
if (user : User.t) == me then Ok () else Error usersdontmatch_get
let require_membership chan me =
match Router.membership chan me with
| mem -> Ok mem
| exception Not_found -> Error (notonchannel (Chan.name chan))
let require_chan_op (m : membership) =
match m.mem_priv with
| Operator -> Ok ()
| _ -> Error (chanoprivsneeded (Chan.name m.mem_chan))
(* modes *)
let set_user_mode ?(add = Mode.Set.empty) ?(rem = Mode.Set.empty) user =
let mode, chg =
Mode.Set.normalize
(User.mode user)
{ add = Mode.Set.remove `o add; rem }
in
if chg <> Mode.Set.no_change then
let modestr = Fmt.str "%a" Mode.Set.pp_change chg in
let msg = Msg.make "MODE" [User.nick user; modestr] in
begin
Router.relay msg ~from:user [`to_self];
User.set_mode user mode;
end
let set_chan_mode ~from ?(add = Mode.Set.empty) ?(rem = Mode.Set.empty) chan =
let mode, chg =
Mode.Set.normalize
(Chan.mode chan)
{ add; rem }
in
if chg <> Mode.Set.no_change then
let modestr = Fmt.str "%a" Mode.Set.pp_change chg in
let msg = Msg.make "MODE" [Chan.name chan; modestr] in
Router.relay msg ~from [`to_chan chan; `to_self];
Chan.set_mode chan mode
let set_chan_key chan ~from chg =
let key, args = match chg with
| `set k -> Some k, ["+k"; k]
| `unset -> None, ["-k"]
in
if key <> Chan.key chan then
let always_trailing = Option.is_some key in
let msg = Msg.make "MODE" (Chan.name chan :: args) ~always_trailing in
Router.relay msg ~from [`to_chan chan; `to_self];
Chan.set_key chan key
let set_chan_limit chan ~from chg =
let limit, args = match chg with
| `set l -> Some l, ["+l"; string_of_int l]
| `unset -> None, ["-l"]
in
if limit <> Chan.limit chan then
let msg = Msg.make "MODE" (Chan.name chan :: args) in
Router.relay msg ~from [`to_chan chan; `to_self];
Chan.set_limit chan limit
let set_member_priv ~from (mem : membership) (priv : Router.priv) =
let user = mem.mem_user in
let chan = mem.mem_chan in
let modestr = match mem.mem_priv, priv with
| _, Voice -> "+v"
| _, Operator -> "+o"
| Voice, Normal -> "-v"
| Operator, Normal -> "-o"
| _, _ -> ""
in
if mem.mem_priv <> priv then
let msg = Msg.make "MODE" [Chan.name chan; modestr; User.nick user] in
Router.relay msg ~from [`to_chan chan; `to_self];
mem.mem_priv <- priv
let on_get_user_mode user me =
let* () = require_same_user user me in
Ok [
"221", [Fmt.str "+%a" Mode.Set.pp (User.mode me)]
]
let on_set_user_mode user me modestr _args =
let* () = require_same_user user me in
let* chg = try Ok (Mode.Parse.user_modes modestr)
with Mode.Parse.Unknown_mode _ ->
(* TODO: "If one or more modes sent are not implemented on the server, the server
MUST apply the modes that are implemented, and then send the ERR_UMODEUNKNOWNFLAG
(501) in reply along with the MODE message." *)
Error umodeunknownflag
in
set_user_mode me ~add:chg.add ~rem:chg.rem;
Ok []
let on_get_chan_mode chan me =
let rpls = [
["324", [Chan.name chan; Fmt.str "+%a" Mode.Set.pp (Chan.mode chan)]];
begin match Chan.limit chan with
| Some lim -> ["324", [Chan.name chan; "+l"; string_of_int lim]]
| None -> []
end;
begin match Chan.key chan with
| Some key ->
let key = match Router.membership chan me with
| _is_member -> key
| exception Not_found -> "*"
in
["324", [Chan.name chan; "+k"; key]]
| None -> []
end;
(* TODO: RPL_CREATIONTIME (329) *)
] in
Ok (List.flatten rpls)
let on_set_chan_mode chan me modestr args ~router =
let* chg = try Ok (Mode.Parse.chan_modes modestr args)
with
| Mode.Parse.Missing_args ->
Error (needmoreparams "MODE")
| Mode.Parse.Unknown_mode ch ->
Error (unknownmode ch)
(* TODO: ERR_INVALIDMODEPARAM (696)
"<client> <target chan/user> <mode char> <parameter> :<description>" *)
in
let* mem = require_membership chan me in
let* () = require_chan_op mem in
set_chan_mode chan ~from:me ~add:chg.chan_modes.add ~rem:chg.chan_modes.rem;
Option.iter (set_chan_key chan ~from:me) chg.chan_key;
Option.iter (set_chan_limit chan ~from:me) chg.chan_limit;
(* TODO: MODE <chan> +b *)
let results =
List.map
(fun (dir, mode, nick) ->
let* user = try Ok (Router.find_user router nick)
with Not_found -> Error (nosuchnick nick) in
let* mem = try Ok (Router.membership chan user)
with Not_found -> Error (usernotinchannel (User.nick user) (Chan.name chan)) in
let priv : Router.priv = match mode with
| `o -> Operator
| `v -> Voice
in
begin match dir with
| `add -> set_member_priv mem priv ~from:me
| `rem -> if mem.mem_priv = priv then set_member_priv mem Normal ~from:me
end;
Ok ())
chg.chan_privs
in
Ok (List.flat_map list_of_errors results)
let on_msg_mode t name args =
let* me = require_registered t in
let* on_set, on_get =
try
match name_type name with
| `nick ->
let u = Router.find_user t.router name in
Ok (on_set_user_mode u, on_get_user_mode u)
| `chan ->
let c = Router.find_chan t.router name in
Ok (on_set_chan_mode c ~router:t.router, on_get_chan_mode c)
| `invalid -> raise Not_found
with Not_found ->
Error (nosuchnick name)
in
let* rpls =
match args with
| [] -> on_get me
| modestr :: args -> on_set me modestr args
in
List.iter (reply t) rpls;
Ok ()
(* messages and channels *)
let on_privmsg_chan from chan =
let cannot_send =
try
let mem = Router.membership chan from in
(* check if moderated (+m) *)
if Mode.Set.mem `m (Chan.mode chan) then
mem.mem_priv < Voice
else
false
with Not_found ->
(* check if no external messages (+n) *)
Mode.Set.mem `n (Chan.mode chan)
in
if cannot_send then
Error (cannotsendtochan (Chan.name chan))
else
Ok (Chan.name chan, [`to_chan chan])
let on_privmsg_user _from user =
match User.away user with
| Some text -> Error (away (User.nick user) text)
| None -> Ok (User.nick user, [`to_user user])
let on_msg_privmsg t tgt txt =
let* me = require_registered t in
let* name, tgts =
try
match name_type tgt with
| `chan -> on_privmsg_chan me (Router.find_chan t.router tgt)
| `nick -> on_privmsg_user me (Router.find_user t.router tgt)
| `invalid -> raise Not_found
with Not_found ->
Error (nosuchnick tgt)
in
let msg = Msg.make "PRIVMSG" [name; txt] ~always_trailing:true in
Router.relay msg ~from:me tgts;
Ok ()
let on_msg_away t status =
let* me = require_registered t in
if status <> User.away me then
begin
let rpl = match status with
| None -> "305", ["You are no longer marked as being away"]
| Some _ -> "306", ["You have been marked as being away"]
in
User.set_away me status;
reply t rpl
end;
Ok ()
let list_names t me chan =
let is_secret = Mode.Set.mem `s (Chan.mode chan) in
let is_invisible user = Mode.Set.mem `i (User.mode user) in
let members =
match Router.membership chan me with
| _is_member -> Chan.membership chan
| exception Not_found ->
if is_secret then
[]
else
Chan.membership_when
(fun (m : membership) ->
not (is_invisible m.mem_user))
chan
in
let nicks =
List.map
(fun (m : membership) ->
let nick = User.nick m.mem_user in
match m.mem_priv with
| Normal -> nick
| Voice -> "+" ^ nick
| Operator -> "@" ^ nick)
members
in
let chan_name = Chan.name chan in
let chan_sym = if is_secret then "@" else "=" in
begin
(* TODO: concat member names until message becomes too long *)
List.iter (fun nick -> reply t ("353", [chan_sym; chan_name; nick])) nicks;
reply t ("366", [chan_name; "End of NAMES list"])
end
let on_msg_names t name =
let* me = require_registered t in
let* chan =
try
match name_type name with
| `chan -> Ok (Router.find_chan t.router name)
| `nick | `invalid -> raise Not_found
with Not_found ->
Error (nosuchchannel name)
in
list_names t me chan;
Ok ()
let get_topic ?(reply_if_missing=true) t chan =
match Chan.topic chan with
| Some topic ->
reply t ("332", [Chan.name chan; topic])
(* TODO: RPL_TOPICWHOTIME ? *)
| None ->
if reply_if_missing then
reply t ("331", [Chan.name chan; "No topic is set"])
let set_topic chan topic =
Chan.set_topic chan topic
let on_msg_topic t name args =
let* me = require_registered t in
let* chan =
try
match name_type name with
| `chan -> Ok (Router.find_chan t.router name)
| `nick | `invalid -> raise Not_found
with Not_found ->
Error (nosuchchannel name)
in
let* mem = require_membership chan me in
match args with
| [] ->
get_topic t chan;
Ok ()
| args ->
let* () =
if Mode.Set.mem `t (Chan.mode chan) then require_chan_op mem
else Ok ()
in
let topic = String.concat " " args in
let msg = Msg.make "TOPIC" [Chan.name chan; topic] ~always_trailing:true in
Router.relay msg ~from:me [`to_chan chan; `to_self];
set_topic chan (if args = [""] then None else Some topic);
Ok ()
let join t user chan =
let msg = Msg.make "JOIN" [Chan.name chan] in
Router.relay msg ~from:user [`to_chan chan; `to_self];
let mem = Router.join chan user in
if not (Chan.is_registered chan ~router:t.router) then
begin
Chan.register chan ~router:t.router;
set_chan_mode chan ~from:user ~add:t.server_info.conf.init_cmode;
set_member_priv mem ~from:user Operator;
end
let on_msg_join t name =
let* me = require_registered t in
let* chan =
try
match name_type name with
| `chan -> Ok (Router.find_chan t.router name)
| `nick | `invalid ->
(* pretend malformed channel name means the channel doesn't exist and
DON'T try to make a new channel *)
Error (nosuchchannel name)
with Not_found ->
debug (fun m -> m "making new channel %S" name);
Ok (Chan.make ~name)
in
match Router.membership chan me with
| _already_a_member -> Ok ()
| exception Not_found ->
if Chan.is_full chan then
Error (channelisfull (Chan.name chan))
else begin
(* TODO: +k *)
_todo_validation_please ();
join t me chan;
get_topic t chan ~reply_if_missing:false;
list_names t me chan;
Ok ()
end
let leave t (mem : membership) ~from ~why =
let user = mem.mem_user in
let chan = mem.mem_chan in
begin match why with
| `quit ->
(* assume QUIT message has already been relayed *)
()
| `part reason ->
let always_trailing = Option.is_some reason in
let params = Chan.name chan :: Option.to_list reason in
let msg = Msg.make "PART" params ~always_trailing in
Router.relay msg ~from [`to_chan chan; `to_self]
| `kick comment ->
let always_trailing = Option.is_some comment in
let params = Chan.name chan :: User.nick user :: Option.to_list comment in
let msg = Msg.make "KICK" params ~always_trailing in
Router.relay msg ~from [`to_chan chan; `to_self]
end;
Router.part mem;
if Chan.is_empty chan then
begin
debug (fun m -> m "recycling empty channel %S" (Chan.name chan));
Chan.unregister chan ~router:t.router;
end
let on_msg_part t name reason =
let* me = require_registered t in
let* chan =
try
match name_type name with
| `chan -> Ok (Router.find_chan t.router name)
| `nick | `invalid -> raise Not_found
with Not_found ->
Error (nosuchchannel name)
in
let* mem = try Ok (Router.membership chan me)
with Not_found -> Error (notonchannel name) in
leave t mem ~from:me ~why:(`part reason);
Ok ()
let on_msg_kick t name nick comment =
let* me = require_registered t in
let* chan =
try
match name_type name with
| `chan -> Ok (Router.find_chan t.router name)
| `nick | `invalid -> raise Not_found
with Not_found ->
Error (nosuchchannel name)
in
let* () = require_membership chan me >>= require_chan_op in
let* user =
try
match name_type nick with
| `nick -> Ok (Router.find_user t.router nick)
| `chan | `invalid -> raise Not_found
with Not_found ->
Error (nosuchnick name)
in
let* mem = try Ok (Router.membership chan user)
with Not_found ->
Error (usernotinchannel (User.nick user) (Chan.name chan))
in
leave t mem ~from:me ~why:(`kick comment);
Ok ()
let on_msg_join_0 t =
(* "JOIN 0" actually means part from all joined channels *)
let* me = require_registered t in
List.iter
(leave t ~from:me ~why:(`part None))
(User.membership me);
Ok ()
(* welcome and quit *)
let motd t =
let s_hostname = t.server_info.hostname in
let s_motd = t.server_info.motd in
begin
reply t ("375", [Fmt.str "- %s Message of the day - " s_hostname]);
List.iter (fun ln -> reply t ("372", ["- " ^ ln])) s_motd;
reply t ("376", ["End of /MOTD command"]);
end
let on_msg_motd t =
let* _me = require_registered t in
motd t;
Ok ()
let on_msg_ping t token =
let* _me = require_registered t in
match token with
| None -> Ok ()
| Some token ->
let prefix = Server_info.prefix t.server_info in
Outbox.send t.outbox
(Msg.make ~prefix "PONG" [t.server_info.hostname; token]
~always_trailing:true);
Ok ()
let on_msg_pong t _token =
let* _me = require_registered t in
Ok ()
let welcome t me =
let whoami = Msg.prefix_string (User.prefix me) in
let s_hostname = t.server_info.hostname in
let s_version = t.server_info.version in
let s_created = t.server_info.created in
let s_conf = t.server_info.conf in
let modes l = String.of_seq (List.to_seq l |> Seq.map Mode.to_char) in
let umodes = modes s_conf.all_umodes in
let cmodes = modes s_conf.all_cmodes in
let pmodes = modes s_conf.all_pmodes in
begin
reply t ("001", [Fmt.str "Welcome to the tali IRC network %s" whoami]);
reply t ("002", [Fmt.str "Your host is %s, running version %s" s_hostname s_version]);
reply t ("003", [Fmt.str "This server was created %s" s_created]);
reply t ("004", [s_hostname; s_version; umodes; cmodes; pmodes]);
reply t ("005", s_conf.isupport @ ["are supported by this server"]);
motd t;
end
let quit t me ~reason =
begin
let msg = Msg.make "QUIT" [User.nick me; reason] ~always_trailing:true in
Router.relay msg ~from:me [`to_interested];
List.iter
(leave t ~from:me ~why:`quit)
(User.membership me);
User.unregister me ~router:t.router;
t.user <- None
end
let close ?(reason = "Client closed") t =
Option.iter (quit t ~reason) t.user;
Outbox.close t.outbox
let on_msg_quit t reason =
let reason = match reason with
| None -> "Quit"
| Some x -> "Quit: " ^ x
in
close t ~reason;
Ok ()
(* user registration *)
let attempt_to_register t =
match t.pending_nick, t.pending_userinfo with
| Some nick, Some userinfo ->
t.pending_nick <- None;
if not (Router.is_nick_available t.router nick) then
Error (nicknameinuse nick)
else
let me = User.make nick ~userinfo ~outbox:t.outbox in
User.register me ~router:t.router;
t.user <- Some me;
welcome t me;
set_user_mode me ~add:t.server_info.conf.init_umode;
Ok ()
| _, _ ->
Ok ()
let user_set_nick t me nick =
if not (Router.is_nick_available t.router nick) then
Error (nicknameinuse nick)
else
begin
let msg = Msg.make "NICK" [nick] in
Router.relay msg ~from:me [`to_interested];
User.unregister me ~router:t.router;
User.set_nick me nick;
User.register me ~router:t.router;
Ok ()
end
let on_msg_nick t nick =
let* () =
match name_type nick with
| `nick -> Ok ()
| `chan | `invalid -> Error (erroneusnickname nick)
in
match t.user with
| Some me ->
user_set_nick t me nick
| None ->
t.pending_nick <- Some nick;
attempt_to_register t
let on_msg_user t username realname =
match t.user with
| Some _me -> Error alreadyregistered
| None ->
(* TODO: configure hiding hostnames *)
let hostname = match t.addr with
| ADDR_INET (ia, _) -> Unix.string_of_inet_addr ia
| ADDR_UNIX path -> path
in
t.pending_userinfo <- Some { username; realname; hostname };
attempt_to_register t
(* message parsing *)
let concat_args = function
| [] | [""] -> None
| xs -> Some (String.concat " " xs)
let dispatch t = function
| "NICK", nick :: _ when nick <> "" -> on_msg_nick t nick
| "NICK", _ -> Error nonicknamegiven
| "USER", unm :: _ :: _ :: rnm :: _ -> on_msg_user t unm rnm
| "QUIT", reason -> on_msg_quit t (concat_args reason)
| "MOTD", _ -> on_msg_motd t
| "PING", args -> on_msg_ping t (concat_args args)
| "PONG", args -> on_msg_pong t (concat_args args)
| "PRIVMSG", ([] | "" :: _) -> Error norecipient
| "PRIVMSG", ([_] | _ :: "" :: _) -> Error notexttosend
| "PRIVMSG", tgt :: msg :: _ -> on_msg_privmsg t tgt msg
| "JOIN", tgt :: _ when tgt <> "" -> on_msg_join t tgt
| "JOIN 0", _ -> (* hack; see split_command_params *) on_msg_join_0 t
| "NAMES", tgt :: _ when tgt <> "" -> on_msg_names t tgt
| "TOPIC", tgt :: args when tgt <> "" -> on_msg_topic t tgt args
| "PART", tgt :: reason when tgt <> "" -> on_msg_part t tgt (concat_args reason)
| "KICK", chn :: tgt :: comment when chn <> "" && tgt <> "" ->
on_msg_kick t chn tgt (concat_args comment)
| "AWAY", args -> on_msg_away t (concat_args args)
| "MODE", tgt :: args when tgt <> "" -> on_msg_mode t tgt args
| ("USER" | "JOIN" | "NAMES" | "PART" | "KICK" | "MODE") as cmd, _ ->
Error (needmoreparams cmd)
| cmd, _ ->
Error (unknowncommand cmd)
let split_command_params cmd params =
match cmd, params with
| "JOIN", "0" :: _ ->
["JOIN 0", []]
| "JOIN", tgts :: rest
when String.contains tgts ',' ->
(* TODO: split <keys> argument as well *)
String.split_on_char ',' tgts |>
List.map (fun tgt -> "JOIN", tgt :: rest)
| ("PRIVMSG" | "NAMES" | "PART"), tgts :: rest
when String.contains tgts ',' ->
(* TODO: "JOIN" should be handled specially *)
String.split_on_char ',' tgts |>
List.map (fun tgt -> cmd, tgt :: rest)
| "KICK", chan :: tgts :: rest
when String.contains tgts ',' ->
String.split_on_char ',' tgts |>
List.map (fun tgt -> "KICK", chan :: tgt :: rest)
| _ ->
[cmd, params]
let pp_args ppf (cmd, params) =
Fmt.pf ppf "@[%s@ %a@]" cmd (Fmt.list (Fmt.fmt "%S") ~sep:Fmt.sp) params
let on_msg t (msg : Msg.t) : unit =
let results =
List.map
(fun cmd ->
trace (fun m -> m "@[%a:@ %a@]" pp_sockaddr t.addr pp_args cmd);
dispatch t cmd)
(split_command_params
msg.command
msg.params)
in
List.iter (reply t)
(List.flat_map list_of_errors results)