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 activity : activity_state; mutable user : User.t option; mutable pending_nick : name option; mutable pending_userinfo : userinfo option; } and activity_state = (* enters this state whenever the client interacts with the server *) | Active (* enters this state after the ping interval has elapsed *) | Inactive (* enters this after the ping interval has elapsed again. the client must respond with a PONG within the next interval or else the connection will be closed *) | Pinged of string let make ~router ~server_info ~addr = { router; server_info; addr; outbox = Outbox.make (); user = None; pending_nick = None; pending_userinfo = None; activity = Active; } 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) " :" *) 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 +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 set_away t me status = if status <> User.away me then begin User.set_away me status; match status with | None -> reply t ("305", ["You are no longer marked as being away"]) | Some _ -> reply t ("306", ["You have been marked as being away"]) end let on_msg_away t status = let* me = require_registered t in set_away t me status; 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) -> Router.membership_prefix m.mem_priv ^ User.nick m.mem_user) 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 () (* user queries *) let user_who_flags user = (* Away status: the letter H ('H', 0x48) to indicate that the user is here, or the letter G ('G', 0x47) to indicate that the user is gone. *) begin match User.away user with | Some _ -> "G" | None -> "H" end ^ (* Optionally, the highest channel membership prefix that the client has in , if the client has one. *) Router.membership_prefix (User.highest_membership_priv user) let list_who t chan users = let chan_str = match chan with | None -> "*" | Some chan -> Chan.name chan in List.iter (fun user -> let server = t.server_info.hostname in let flags = user_who_flags user in let { username; hostname; realname } = user.userinfo in reply t ("352", [chan_str; username; hostname; server; User.nick user; flags; "0 " ^ realname])) users let on_msg_who t mask = let* me = require_registered t in let* chan, users = try match name_type mask with | `nick -> let user = Router.find_user t.router mask in let chan = try Some (User.find_common_channel user me) with Not_found -> None in Ok (chan, [user]) | `chan -> let chan = Router.find_chan t.router mask in Ok (Some chan, Chan.members chan) | `invalid -> raise Not_found with Not_found -> Ok (None, []) in list_who t chan users; reply t ("315", [mask; "End of WHO list"]); 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 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 (* ping *) 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 match t.activity with | Active | Inactive -> trace (fun m -> m "%a:@ ignored pong" pp_sockaddr t.addr); Ok () | Pinged token' -> if token <> Some token' then debug (fun m -> m "%a:@ got weird PONG token" pp_sockaddr t.addr); Ok () let on_ping t = match t.activity with | _ when Outbox.is_closed t.outbox -> trace (fun m -> m "%a:@ connection stale" pp_sockaddr t.addr); Error () | Active -> trace (fun m -> m "%a:@ inactive" pp_sockaddr t.addr); t.activity <- Inactive; Ok () | Inactive -> let token = "meow" in trace (fun m -> m "%a:@ ping %S" pp_sockaddr t.addr token); t.activity <- Pinged token; if Option.is_some t.user then begin let prefix = Server_info.prefix t.server_info in Outbox.send t.outbox (Msg.make ~prefix "PING" [token] ~always_trailing:true); end; Ok () | Pinged _ -> debug (fun m -> m "%a:@ timed out" pp_sockaddr t.addr); Error () (* 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 | "WHO", mask :: _ when mask <> "" -> on_msg_who t mask | ("USER" | "JOIN" | "NAMES" | "PART" | "KICK" | "MODE" | "WHO") as cmd, _ -> Error (needmoreparams cmd) (* TODO: "LIST" *) (* TODO: "ADMIN" *) (* TODO: "CONNECT" *) (* TODO: "LUSERS" *) (* TODO: "TIME" *) (* TODO: "STATS" *) (* TODO: "HELP" *) (* TODO: "INFO" *) (* TODO: "NOTICE" *) (* TODO: "WHOIS" *) (* TODO: "WHOWAS" *) (* TODO: "KILL" *) (* TODO: "REHASH" *) (* TODO: "RESTART" *) (* TODO: "SQUIT" *) (* TODO: "LINKS" *) (* TODO: "USERHOST" *) (* TODO: "WALLOPS" *) | 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 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); t.activity <- Active