diff --git a/ChangeLog b/ChangeLog index e5d031f5..f35434c1 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,41 @@ // $Id: ChangeLog,v 1.173 2010/04/10 18:56:06 danielaustin Exp $ // +2026-01-09 MrIron + * Major update: TLS support (based on Compy's initial implementation), SASL/SCRAM authentication, fingerprint features, and expanded user/channel modes. + * configure.ac: + - Added default libssl detection and configuration for TLS features. + * bin/server_command_map, include/Network.h, include/events.h, libircu/Makefile.am, libircu/msg_CF.cc (new), src/Network.cc: + - Implemented new CF network command (network configuration) to hold network-wide configuration variables, necessary for the SASL implementation. + * src/main.cc, src/server.cc, src/client.cc, src/Channel.cc, src/Network.cc, src/iClient.cc, libgnuworld/Connection.cc, Connection.h, ConnectionManager.cc, ConnectionManager.h, misc.cc, misc.h, + include/Channel.h, include/Network.h, include/defs.h.in, include/events.h, include/gnuworld_config.h, include/iClient.h, include/server.h: + - Added TLS support (configurable, with certificate/key loading, TLS-only channel/user modes). + - Added new channel and user modes for TLS implementation. + - Updated connection logic, mode parsing, and display for TLS. + - Added TLS fingerprint field to iClient and related constructors. + - Updated for new TLS, fingerprint, and mode features. + * mod.cservice/cservice_crypt.cc, mod.cservice/cservice_crypt.h (new files): + - Implemented SCRAM-SHA-256 password hashing and authentication helpers. + - Added base64 helpers, PBKDF2, HMAC, and nonce generation. + * mod.cservice/CERTCommand.cc (new file): + - Added certificate management and fingerprint registration commands. + * mod.cservice/CHANINFOCommand.cc, CLEARMODECommand.cc, HELLOCommand.cc, LOGINCommand.cc, NEWPASSCommand.cc, SETCommand.cc, constants.h, cservice.cc, cservice.h, cserviceCommands.h, cservice_config.h, cservice_confvars.h, responses.h, sqlUser.cc, sqlUser.h: + - Added SASL/SCRAM authentication, fingerprint management, new user flags (certonly, autohide, etc.), and expanded command set. + - Improved error handling, logging, and configuration options. + * mod.dronescan/FAKECommand.cc, mod.gnutest/gnutest.cc, mod.nickserv/nickserv.cc, mod.snoop/snoop.cc: + - Updated iClient instantiation to include TLS fingerprint field. + * mod.scanner/wingateModule.cc: + - Updated Connect() to accept TLS flag. + * mod.ccontrol/CLEARCHANCommand.cc, WHOISCommand.cc, ccontrol.cc: + - Minor updates for compatibility with new TLS and fingerprint features. + * mod.cloner/cloner.cc: + - Minor update for compatibility. + * libircu/msg_B.cc, msg_CM.cc, msg_M.cc, msg_N.cc: + - Implemented TLS user and channel modes and fingerprint parsing. + * bin/GNUWorld.example.conf, bin/cservice.example.conf: + - Updated example configs for new TLS and authentication options. + * doc/cservice.sql, doc/cservice.update.sql: + - Updated schema for SCRAM, fingerprint, and new flags. + 2025-10-17 MrIron * src/main.cc: * include/server.h: diff --git a/Makefile.in b/Makefile.in index ccc09957..f8b2560e 100644 --- a/Makefile.in +++ b/Makefile.in @@ -153,6 +153,8 @@ bin_PROGRAMS = gnuworld$(EXEEXT) @COND_MODCSERVICE_TRUE@ mod.cservice/constants.h \ @COND_MODCSERVICE_TRUE@ mod.cservice/cserviceCommands.h \ @COND_MODCSERVICE_TRUE@ mod.cservice/cservice_config.h \ +@COND_MODCSERVICE_TRUE@ mod.cservice/cservice_confvars.h \ +@COND_MODCSERVICE_TRUE@ mod.cservice/cservice_crypt.h \ @COND_MODCSERVICE_TRUE@ mod.cservice/cservice.h \ @COND_MODCSERVICE_TRUE@ mod.cservice/levels.h \ @COND_MODCSERVICE_TRUE@ mod.cservice/networkData.h \ @@ -562,15 +564,16 @@ libcloner_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) \ @COND_MODCLONER_TRUE@am_libcloner_la_rpath = -rpath $(libdir) @COND_MODCSERVICE_TRUE@libcservice_la_DEPENDENCIES = libgnuworldDB.la am__libcservice_la_SOURCES_DIST = mod.cservice/banMatcher.cc \ - mod.cservice/cservice.cc mod.cservice/networkData.cc \ - mod.cservice/sqlChannel.cc mod.cservice/sqlUser.cc \ - mod.cservice/sqlLevel.cc mod.cservice/sqlBan.cc \ - mod.cservice/sqlPendingChannel.cc \ + mod.cservice/cservice.cc mod.cservice/cservice_crypt.cc \ + mod.cservice/networkData.cc mod.cservice/sqlChannel.cc \ + mod.cservice/sqlUser.cc mod.cservice/sqlLevel.cc \ + mod.cservice/sqlBan.cc mod.cservice/sqlPendingChannel.cc \ mod.cservice/sqlPendingTraffic.cc mod.cservice/csGline.cc \ mod.cservice/ACCESSCommand.cc \ mod.cservice/ADDCOMMENTCommand.cc \ mod.cservice/ADDUSERCommand.cc mod.cservice/BANCommand.cc \ - mod.cservice/BANLISTCommand.cc mod.cservice/CHANINFOCommand.cc \ + mod.cservice/BANLISTCommand.cc mod.cservice/CERTCommand.cc \ + mod.cservice/CHANINFOCommand.cc \ mod.cservice/CLEARMODECommand.cc mod.cservice/DEOPCommand.cc \ mod.cservice/DEVOICECommand.cc mod.cservice/FORCECommand.cc \ mod.cservice/HELPCommand.cc mod.cservice/HELLOCommand.cc \ @@ -602,6 +605,7 @@ am__libcservice_la_SOURCES_DIST = mod.cservice/banMatcher.cc \ mod.cservice/MODECommand.cc @COND_MODCSERVICE_TRUE@am_libcservice_la_OBJECTS = mod.cservice/libcservice_la-banMatcher.lo \ @COND_MODCSERVICE_TRUE@ mod.cservice/libcservice_la-cservice.lo \ +@COND_MODCSERVICE_TRUE@ mod.cservice/libcservice_la-cservice_crypt.lo \ @COND_MODCSERVICE_TRUE@ mod.cservice/libcservice_la-networkData.lo \ @COND_MODCSERVICE_TRUE@ mod.cservice/libcservice_la-sqlChannel.lo \ @COND_MODCSERVICE_TRUE@ mod.cservice/libcservice_la-sqlUser.lo \ @@ -615,6 +619,7 @@ am__libcservice_la_SOURCES_DIST = mod.cservice/banMatcher.cc \ @COND_MODCSERVICE_TRUE@ mod.cservice/libcservice_la-ADDUSERCommand.lo \ @COND_MODCSERVICE_TRUE@ mod.cservice/libcservice_la-BANCommand.lo \ @COND_MODCSERVICE_TRUE@ mod.cservice/libcservice_la-BANLISTCommand.lo \ +@COND_MODCSERVICE_TRUE@ mod.cservice/libcservice_la-CERTCommand.lo \ @COND_MODCSERVICE_TRUE@ mod.cservice/libcservice_la-CHANINFOCommand.lo \ @COND_MODCSERVICE_TRUE@ mod.cservice/libcservice_la-CLEARMODECommand.lo \ @COND_MODCSERVICE_TRUE@ mod.cservice/libcservice_la-DEOPCommand.lo \ @@ -775,19 +780,20 @@ libgnuworldcore_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CXX \ $(libgnuworldcore_la_LDFLAGS) $(LDFLAGS) -o $@ libircu_la_LIBADD = am_libircu_la_OBJECTS = libircu/la-msg_AC.lo libircu/la-msg_AD.lo \ - libircu/la-msg_B.lo libircu/la-msg_C.lo libircu/la-msg_CM.lo \ - libircu/la-msg_D.lo libircu/la-msg_DS.lo libircu/la-msg_EA.lo \ - libircu/la-msg_EB.lo libircu/la-msg_G.lo libircu/la-msg_GL.lo \ - libircu/la-msg_I.lo libircu/la-msg_J.lo libircu/la-msg_JU.lo \ - libircu/la-msg_K.lo libircu/la-msg_L.lo libircu/la-msg_M351.lo \ - libircu/la-msg_M391.lo libircu/la-msg_M.lo libircu/la-msg_N.lo \ - libircu/la-msg_NOOP.lo libircu/la-msg_O.lo \ - libircu/la-msg_PA.lo libircu/la-msg_P.lo libircu/la-msg_Q.lo \ - libircu/la-msg_R.lo libircu/la-msg_RI.lo libircu/la-msg_RO.lo \ - libircu/la-msg_S.lo libircu/la-msg_Server.lo \ - libircu/la-msg_SQ.lo libircu/la-msg_T.lo libircu/la-msg_V.lo \ - libircu/la-msg_WA.lo libircu/la-msg_W.lo libircu/la-msg_XQ.lo \ - libircu/la-msg_XR.lo libircu/la-msg_Y.lo + libircu/la-msg_B.lo libircu/la-msg_C.lo libircu/la-msg_CF.lo \ + libircu/la-msg_CM.lo libircu/la-msg_D.lo libircu/la-msg_DS.lo \ + libircu/la-msg_EA.lo libircu/la-msg_EB.lo libircu/la-msg_G.lo \ + libircu/la-msg_GL.lo libircu/la-msg_I.lo libircu/la-msg_J.lo \ + libircu/la-msg_JU.lo libircu/la-msg_K.lo libircu/la-msg_L.lo \ + libircu/la-msg_M351.lo libircu/la-msg_M391.lo \ + libircu/la-msg_M.lo libircu/la-msg_N.lo libircu/la-msg_NOOP.lo \ + libircu/la-msg_O.lo libircu/la-msg_PA.lo libircu/la-msg_P.lo \ + libircu/la-msg_Q.lo libircu/la-msg_R.lo libircu/la-msg_RI.lo \ + libircu/la-msg_RO.lo libircu/la-msg_S.lo \ + libircu/la-msg_Server.lo libircu/la-msg_SQ.lo \ + libircu/la-msg_T.lo libircu/la-msg_V.lo libircu/la-msg_WA.lo \ + libircu/la-msg_W.lo libircu/la-msg_XQ.lo libircu/la-msg_XR.lo \ + libircu/la-msg_Y.lo libircu_la_OBJECTS = $(am_libircu_la_OBJECTS) libircu_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) \ $(LIBTOOLFLAGS) --mode=link $(CXXLD) $(libircu_la_CXXFLAGS) \ @@ -1011,8 +1017,9 @@ am__depfiles_remade = db/$(DEPDIR)/libgnuworldDB_la-gnuworldDB.Plo \ libgnuworld/$(DEPDIR)/la-threadworker.Plo \ libircu/$(DEPDIR)/la-msg_AC.Plo \ libircu/$(DEPDIR)/la-msg_AD.Plo libircu/$(DEPDIR)/la-msg_B.Plo \ - libircu/$(DEPDIR)/la-msg_C.Plo libircu/$(DEPDIR)/la-msg_CM.Plo \ - libircu/$(DEPDIR)/la-msg_D.Plo libircu/$(DEPDIR)/la-msg_DS.Plo \ + libircu/$(DEPDIR)/la-msg_C.Plo libircu/$(DEPDIR)/la-msg_CF.Plo \ + libircu/$(DEPDIR)/la-msg_CM.Plo libircu/$(DEPDIR)/la-msg_D.Plo \ + libircu/$(DEPDIR)/la-msg_DS.Plo \ libircu/$(DEPDIR)/la-msg_EA.Plo \ libircu/$(DEPDIR)/la-msg_EB.Plo libircu/$(DEPDIR)/la-msg_G.Plo \ libircu/$(DEPDIR)/la-msg_GL.Plo libircu/$(DEPDIR)/la-msg_I.Plo \ @@ -1121,6 +1128,7 @@ am__depfiles_remade = db/$(DEPDIR)/libgnuworldDB_la-gnuworldDB.Plo \ mod.cservice/$(DEPDIR)/libcservice_la-ADDUSERCommand.Plo \ mod.cservice/$(DEPDIR)/libcservice_la-BANCommand.Plo \ mod.cservice/$(DEPDIR)/libcservice_la-BANLISTCommand.Plo \ + mod.cservice/$(DEPDIR)/libcservice_la-CERTCommand.Plo \ mod.cservice/$(DEPDIR)/libcservice_la-CHANINFOCommand.Plo \ mod.cservice/$(DEPDIR)/libcservice_la-CLEARMODECommand.Plo \ mod.cservice/$(DEPDIR)/libcservice_la-DEOPCommand.Plo \ @@ -1176,6 +1184,7 @@ am__depfiles_remade = db/$(DEPDIR)/libgnuworldDB_la-gnuworldDB.Plo \ mod.cservice/$(DEPDIR)/libcservice_la-banMatcher.Plo \ mod.cservice/$(DEPDIR)/libcservice_la-csGline.Plo \ mod.cservice/$(DEPDIR)/libcservice_la-cservice.Plo \ + mod.cservice/$(DEPDIR)/libcservice_la-cservice_crypt.Plo \ mod.cservice/$(DEPDIR)/libcservice_la-networkData.Plo \ mod.cservice/$(DEPDIR)/libcservice_la-sqlBan.Plo \ mod.cservice/$(DEPDIR)/libcservice_la-sqlChannel.Plo \ @@ -1700,8 +1709,8 @@ EXTRA_DIST = bin/ccontrol.example.conf bin/chanfix.example.conf \ libgnuworld/threadworker.h libgnuworld/misc.h \ libgnuworld/MTrie.h libgnuworld/MTrie.cc libgnuworld/Signal.h \ libgnuworld/StringTokenizer.h libgnuworld/xparameters.h \ - libgnuworld/logger.h libgnuworld/notifier.h \ - libgnuworld/prometheus.h libgnuworld/pushover.h \ + libnotifier/logger.h libnotifier/notifier.h \ + libnotifier/prometheus.h libnotifier/pushover.h \ $(am__append_6) $(am__append_8) $(am__append_10) \ $(am__append_12) $(am__append_14) $(am__append_16) \ $(am__append_18) $(am__append_20) $(am__append_22) \ @@ -1751,6 +1760,7 @@ libircu_la_SOURCES = \ libircu/msg_AD.cc \ libircu/msg_B.cc \ libircu/msg_C.cc \ + libircu/msg_CF.cc \ libircu/msg_CM.cc \ libircu/msg_D.cc \ libircu/msg_DS.cc \ @@ -1985,6 +1995,7 @@ gnuworld_LDADD = libgnuworld.la libgnuworldcore.la $(LIBLTDL) @COND_MODCSERVICE_TRUE@libcservice_la_SOURCES = \ @COND_MODCSERVICE_TRUE@ mod.cservice/banMatcher.cc \ @COND_MODCSERVICE_TRUE@ mod.cservice/cservice.cc \ +@COND_MODCSERVICE_TRUE@ mod.cservice/cservice_crypt.cc \ @COND_MODCSERVICE_TRUE@ mod.cservice/networkData.cc \ @COND_MODCSERVICE_TRUE@ mod.cservice/sqlChannel.cc \ @COND_MODCSERVICE_TRUE@ mod.cservice/sqlUser.cc \ @@ -1998,6 +2009,7 @@ gnuworld_LDADD = libgnuworld.la libgnuworldcore.la $(LIBLTDL) @COND_MODCSERVICE_TRUE@ mod.cservice/ADDUSERCommand.cc \ @COND_MODCSERVICE_TRUE@ mod.cservice/BANCommand.cc \ @COND_MODCSERVICE_TRUE@ mod.cservice/BANLISTCommand.cc \ +@COND_MODCSERVICE_TRUE@ mod.cservice/CERTCommand.cc \ @COND_MODCSERVICE_TRUE@ mod.cservice/CHANINFOCommand.cc \ @COND_MODCSERVICE_TRUE@ mod.cservice/CLEARMODECommand.cc \ @COND_MODCSERVICE_TRUE@ mod.cservice/DEOPCommand.cc \ @@ -2753,6 +2765,9 @@ mod.cservice/libcservice_la-banMatcher.lo: \ mod.cservice/$(DEPDIR)/$(am__dirstamp) mod.cservice/libcservice_la-cservice.lo: mod.cservice/$(am__dirstamp) \ mod.cservice/$(DEPDIR)/$(am__dirstamp) +mod.cservice/libcservice_la-cservice_crypt.lo: \ + mod.cservice/$(am__dirstamp) \ + mod.cservice/$(DEPDIR)/$(am__dirstamp) mod.cservice/libcservice_la-networkData.lo: \ mod.cservice/$(am__dirstamp) \ mod.cservice/$(DEPDIR)/$(am__dirstamp) @@ -2788,6 +2803,9 @@ mod.cservice/libcservice_la-BANCommand.lo: \ mod.cservice/libcservice_la-BANLISTCommand.lo: \ mod.cservice/$(am__dirstamp) \ mod.cservice/$(DEPDIR)/$(am__dirstamp) +mod.cservice/libcservice_la-CERTCommand.lo: \ + mod.cservice/$(am__dirstamp) \ + mod.cservice/$(DEPDIR)/$(am__dirstamp) mod.cservice/libcservice_la-CHANINFOCommand.lo: \ mod.cservice/$(am__dirstamp) \ mod.cservice/$(DEPDIR)/$(am__dirstamp) @@ -3140,6 +3158,8 @@ libircu/la-msg_B.lo: libircu/$(am__dirstamp) \ libircu/$(DEPDIR)/$(am__dirstamp) libircu/la-msg_C.lo: libircu/$(am__dirstamp) \ libircu/$(DEPDIR)/$(am__dirstamp) +libircu/la-msg_CF.lo: libircu/$(am__dirstamp) \ + libircu/$(DEPDIR)/$(am__dirstamp) libircu/la-msg_CM.lo: libircu/$(am__dirstamp) \ libircu/$(DEPDIR)/$(am__dirstamp) libircu/la-msg_D.lo: libircu/$(am__dirstamp) \ @@ -3470,6 +3490,7 @@ distclean-compile: @AMDEP_TRUE@@am__include@ @am__quote@libircu/$(DEPDIR)/la-msg_AD.Plo@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@libircu/$(DEPDIR)/la-msg_B.Plo@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@libircu/$(DEPDIR)/la-msg_C.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@libircu/$(DEPDIR)/la-msg_CF.Plo@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@libircu/$(DEPDIR)/la-msg_CM.Plo@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@libircu/$(DEPDIR)/la-msg_D.Plo@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@libircu/$(DEPDIR)/la-msg_DS.Plo@am__quote@ # am--include-marker @@ -3592,6 +3613,7 @@ distclean-compile: @AMDEP_TRUE@@am__include@ @am__quote@mod.cservice/$(DEPDIR)/libcservice_la-ADDUSERCommand.Plo@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@mod.cservice/$(DEPDIR)/libcservice_la-BANCommand.Plo@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@mod.cservice/$(DEPDIR)/libcservice_la-BANLISTCommand.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@mod.cservice/$(DEPDIR)/libcservice_la-CERTCommand.Plo@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@mod.cservice/$(DEPDIR)/libcservice_la-CHANINFOCommand.Plo@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@mod.cservice/$(DEPDIR)/libcservice_la-CLEARMODECommand.Plo@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@mod.cservice/$(DEPDIR)/libcservice_la-DEOPCommand.Plo@am__quote@ # am--include-marker @@ -3647,6 +3669,7 @@ distclean-compile: @AMDEP_TRUE@@am__include@ @am__quote@mod.cservice/$(DEPDIR)/libcservice_la-banMatcher.Plo@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@mod.cservice/$(DEPDIR)/libcservice_la-csGline.Plo@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@mod.cservice/$(DEPDIR)/libcservice_la-cservice.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@mod.cservice/$(DEPDIR)/libcservice_la-cservice_crypt.Plo@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@mod.cservice/$(DEPDIR)/libcservice_la-networkData.Plo@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@mod.cservice/$(DEPDIR)/libcservice_la-sqlBan.Plo@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@mod.cservice/$(DEPDIR)/libcservice_la-sqlChannel.Plo@am__quote@ # am--include-marker @@ -4719,6 +4742,13 @@ mod.cservice/libcservice_la-cservice.lo: mod.cservice/cservice.cc @AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ @am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libcservice_la_CXXFLAGS) $(CXXFLAGS) -c -o mod.cservice/libcservice_la-cservice.lo `test -f 'mod.cservice/cservice.cc' || echo '$(srcdir)/'`mod.cservice/cservice.cc +mod.cservice/libcservice_la-cservice_crypt.lo: mod.cservice/cservice_crypt.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libcservice_la_CXXFLAGS) $(CXXFLAGS) -MT mod.cservice/libcservice_la-cservice_crypt.lo -MD -MP -MF mod.cservice/$(DEPDIR)/libcservice_la-cservice_crypt.Tpo -c -o mod.cservice/libcservice_la-cservice_crypt.lo `test -f 'mod.cservice/cservice_crypt.cc' || echo '$(srcdir)/'`mod.cservice/cservice_crypt.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) mod.cservice/$(DEPDIR)/libcservice_la-cservice_crypt.Tpo mod.cservice/$(DEPDIR)/libcservice_la-cservice_crypt.Plo +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='mod.cservice/cservice_crypt.cc' object='mod.cservice/libcservice_la-cservice_crypt.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libcservice_la_CXXFLAGS) $(CXXFLAGS) -c -o mod.cservice/libcservice_la-cservice_crypt.lo `test -f 'mod.cservice/cservice_crypt.cc' || echo '$(srcdir)/'`mod.cservice/cservice_crypt.cc + mod.cservice/libcservice_la-networkData.lo: mod.cservice/networkData.cc @am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libcservice_la_CXXFLAGS) $(CXXFLAGS) -MT mod.cservice/libcservice_la-networkData.lo -MD -MP -MF mod.cservice/$(DEPDIR)/libcservice_la-networkData.Tpo -c -o mod.cservice/libcservice_la-networkData.lo `test -f 'mod.cservice/networkData.cc' || echo '$(srcdir)/'`mod.cservice/networkData.cc @am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) mod.cservice/$(DEPDIR)/libcservice_la-networkData.Tpo mod.cservice/$(DEPDIR)/libcservice_la-networkData.Plo @@ -4810,6 +4840,13 @@ mod.cservice/libcservice_la-BANLISTCommand.lo: mod.cservice/BANLISTCommand.cc @AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ @am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libcservice_la_CXXFLAGS) $(CXXFLAGS) -c -o mod.cservice/libcservice_la-BANLISTCommand.lo `test -f 'mod.cservice/BANLISTCommand.cc' || echo '$(srcdir)/'`mod.cservice/BANLISTCommand.cc +mod.cservice/libcservice_la-CERTCommand.lo: mod.cservice/CERTCommand.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libcservice_la_CXXFLAGS) $(CXXFLAGS) -MT mod.cservice/libcservice_la-CERTCommand.lo -MD -MP -MF mod.cservice/$(DEPDIR)/libcservice_la-CERTCommand.Tpo -c -o mod.cservice/libcservice_la-CERTCommand.lo `test -f 'mod.cservice/CERTCommand.cc' || echo '$(srcdir)/'`mod.cservice/CERTCommand.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) mod.cservice/$(DEPDIR)/libcservice_la-CERTCommand.Tpo mod.cservice/$(DEPDIR)/libcservice_la-CERTCommand.Plo +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='mod.cservice/CERTCommand.cc' object='mod.cservice/libcservice_la-CERTCommand.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libcservice_la_CXXFLAGS) $(CXXFLAGS) -c -o mod.cservice/libcservice_la-CERTCommand.lo `test -f 'mod.cservice/CERTCommand.cc' || echo '$(srcdir)/'`mod.cservice/CERTCommand.cc + mod.cservice/libcservice_la-CHANINFOCommand.lo: mod.cservice/CHANINFOCommand.cc @am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libcservice_la_CXXFLAGS) $(CXXFLAGS) -MT mod.cservice/libcservice_la-CHANINFOCommand.lo -MD -MP -MF mod.cservice/$(DEPDIR)/libcservice_la-CHANINFOCommand.Tpo -c -o mod.cservice/libcservice_la-CHANINFOCommand.lo `test -f 'mod.cservice/CHANINFOCommand.cc' || echo '$(srcdir)/'`mod.cservice/CHANINFOCommand.cc @am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) mod.cservice/$(DEPDIR)/libcservice_la-CHANINFOCommand.Tpo mod.cservice/$(DEPDIR)/libcservice_la-CHANINFOCommand.Plo @@ -5587,6 +5624,13 @@ libircu/la-msg_C.lo: libircu/msg_C.cc @AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ @am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libircu_la_CXXFLAGS) $(CXXFLAGS) -c -o libircu/la-msg_C.lo `test -f 'libircu/msg_C.cc' || echo '$(srcdir)/'`libircu/msg_C.cc +libircu/la-msg_CF.lo: libircu/msg_CF.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libircu_la_CXXFLAGS) $(CXXFLAGS) -MT libircu/la-msg_CF.lo -MD -MP -MF libircu/$(DEPDIR)/la-msg_CF.Tpo -c -o libircu/la-msg_CF.lo `test -f 'libircu/msg_CF.cc' || echo '$(srcdir)/'`libircu/msg_CF.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) libircu/$(DEPDIR)/la-msg_CF.Tpo libircu/$(DEPDIR)/la-msg_CF.Plo +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='libircu/msg_CF.cc' object='libircu/la-msg_CF.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libircu_la_CXXFLAGS) $(CXXFLAGS) -c -o libircu/la-msg_CF.lo `test -f 'libircu/msg_CF.cc' || echo '$(srcdir)/'`libircu/msg_CF.cc + libircu/la-msg_CM.lo: libircu/msg_CM.cc @am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libircu_la_CXXFLAGS) $(CXXFLAGS) -MT libircu/la-msg_CM.lo -MD -MP -MF libircu/$(DEPDIR)/la-msg_CM.Tpo -c -o libircu/la-msg_CM.lo `test -f 'libircu/msg_CM.cc' || echo '$(srcdir)/'`libircu/msg_CM.cc @am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) libircu/$(DEPDIR)/la-msg_CM.Tpo libircu/$(DEPDIR)/la-msg_CM.Plo @@ -6634,6 +6678,7 @@ distclean: distclean-recursive -rm -f libircu/$(DEPDIR)/la-msg_AD.Plo -rm -f libircu/$(DEPDIR)/la-msg_B.Plo -rm -f libircu/$(DEPDIR)/la-msg_C.Plo + -rm -f libircu/$(DEPDIR)/la-msg_CF.Plo -rm -f libircu/$(DEPDIR)/la-msg_CM.Plo -rm -f libircu/$(DEPDIR)/la-msg_D.Plo -rm -f libircu/$(DEPDIR)/la-msg_DS.Plo @@ -6756,6 +6801,7 @@ distclean: distclean-recursive -rm -f mod.cservice/$(DEPDIR)/libcservice_la-ADDUSERCommand.Plo -rm -f mod.cservice/$(DEPDIR)/libcservice_la-BANCommand.Plo -rm -f mod.cservice/$(DEPDIR)/libcservice_la-BANLISTCommand.Plo + -rm -f mod.cservice/$(DEPDIR)/libcservice_la-CERTCommand.Plo -rm -f mod.cservice/$(DEPDIR)/libcservice_la-CHANINFOCommand.Plo -rm -f mod.cservice/$(DEPDIR)/libcservice_la-CLEARMODECommand.Plo -rm -f mod.cservice/$(DEPDIR)/libcservice_la-DEOPCommand.Plo @@ -6811,6 +6857,7 @@ distclean: distclean-recursive -rm -f mod.cservice/$(DEPDIR)/libcservice_la-banMatcher.Plo -rm -f mod.cservice/$(DEPDIR)/libcservice_la-csGline.Plo -rm -f mod.cservice/$(DEPDIR)/libcservice_la-cservice.Plo + -rm -f mod.cservice/$(DEPDIR)/libcservice_la-cservice_crypt.Plo -rm -f mod.cservice/$(DEPDIR)/libcservice_la-networkData.Plo -rm -f mod.cservice/$(DEPDIR)/libcservice_la-sqlBan.Plo -rm -f mod.cservice/$(DEPDIR)/libcservice_la-sqlChannel.Plo @@ -7008,6 +7055,7 @@ maintainer-clean: maintainer-clean-recursive -rm -f libircu/$(DEPDIR)/la-msg_AD.Plo -rm -f libircu/$(DEPDIR)/la-msg_B.Plo -rm -f libircu/$(DEPDIR)/la-msg_C.Plo + -rm -f libircu/$(DEPDIR)/la-msg_CF.Plo -rm -f libircu/$(DEPDIR)/la-msg_CM.Plo -rm -f libircu/$(DEPDIR)/la-msg_D.Plo -rm -f libircu/$(DEPDIR)/la-msg_DS.Plo @@ -7130,6 +7178,7 @@ maintainer-clean: maintainer-clean-recursive -rm -f mod.cservice/$(DEPDIR)/libcservice_la-ADDUSERCommand.Plo -rm -f mod.cservice/$(DEPDIR)/libcservice_la-BANCommand.Plo -rm -f mod.cservice/$(DEPDIR)/libcservice_la-BANLISTCommand.Plo + -rm -f mod.cservice/$(DEPDIR)/libcservice_la-CERTCommand.Plo -rm -f mod.cservice/$(DEPDIR)/libcservice_la-CHANINFOCommand.Plo -rm -f mod.cservice/$(DEPDIR)/libcservice_la-CLEARMODECommand.Plo -rm -f mod.cservice/$(DEPDIR)/libcservice_la-DEOPCommand.Plo @@ -7185,6 +7234,7 @@ maintainer-clean: maintainer-clean-recursive -rm -f mod.cservice/$(DEPDIR)/libcservice_la-banMatcher.Plo -rm -f mod.cservice/$(DEPDIR)/libcservice_la-csGline.Plo -rm -f mod.cservice/$(DEPDIR)/libcservice_la-cservice.Plo + -rm -f mod.cservice/$(DEPDIR)/libcservice_la-cservice_crypt.Plo -rm -f mod.cservice/$(DEPDIR)/libcservice_la-networkData.Plo -rm -f mod.cservice/$(DEPDIR)/libcservice_la-sqlBan.Plo -rm -f mod.cservice/$(DEPDIR)/libcservice_la-sqlChannel.Plo diff --git a/bin/GNUWorld.example.conf b/bin/GNUWorld.example.conf index 9d5c3845..ee1f00d5 100644 --- a/bin/GNUWorld.example.conf +++ b/bin/GNUWorld.example.conf @@ -9,6 +9,20 @@ name = services.undernet.org description = UnderNet Services numeric = 51 +# Enable TLS connections to the uplink IRC server +# This will require openssl libraries to be installed on the GNUWorld +# host server +tls = yes + +# A valid file path to the private key file for GNUWorld +# Required if tls is enabled +tlsKeyFile = gnuworld.key + +# A valid file path to the public key file for GNUWorld that will +# be exchanged with the uplink. +# Required if tls is enabled +tlsCertFile = gnuworld.cert + # Set this variable to yes if you want the server to attempt # to auto_reconnect when a connection is terminated, set # to no otherwise. diff --git a/bin/cservice.example.conf b/bin/cservice.example.conf index b4b7ff0d..0a73ff2b 100644 --- a/bin/cservice.example.conf +++ b/bin/cservice.example.conf @@ -393,6 +393,13 @@ log_to_admin_console = 1 # chanfix servername to send and receive Oplist/Score requests/answers chanfix_servername = gnuworld6.undernet.org +# Maximum number of fingerprints per user + +max_fingerprints = 10 + +# Timeout for a SASL authentication session (in seconds) +sasl_timeout = 30 + # # Optional settings to enable Pushover notifications. # To use this, you need to create an application on Pushover diff --git a/bin/server_command_map b/bin/server_command_map index 6d3cb4b6..9103bd0f 100644 --- a/bin/server_command_map +++ b/bin/server_command_map @@ -10,6 +10,7 @@ libircu msg_AC AC libircu msg_AD AD libircu msg_B B libircu msg_C C +libircu msg_CF CF libircu msg_CM CM libircu msg_D D libircu msg_NOOP DE diff --git a/configure b/configure index 28957857..2ea2a4f2 100755 --- a/configure +++ b/configure @@ -891,6 +891,9 @@ with_pgconfig with_log4cplus with_log4cplus_lib with_log4cplus_include +with_libssl +with_libssl_lib +with_libssl_include with_extra_includes with_extra_libraries with_boost @@ -1595,6 +1598,11 @@ Optional Packages: Specify location to find liblog4cplus.so --with-log4cplus-include=LOG4CPLUSINCLUDEDIR Specify location to find logger.h + --with-libssl Enable OpenSSL (default) + --with-libssl-lib=LIBSSLLIBDIR + Specify location to find libssl.so + --with-libssl-include=SSLINCLUDEDIR + Specify location to find ssl.h --with-extra-includes=INCLUDESDIR Specify location to find additional include files --with-extra-libraries=LIBRARYDIR @@ -21669,6 +21677,107 @@ fi +# Check whether --with-libssl was given. +if test ${with_libssl+y} +then : + withval=$with_libssl; check_libssl=$withval +else case e in #( + e) check_libssl=yes ;; +esac +fi + + + +# Check whether --with-libssl-lib was given. +if test ${with_libssl_lib+y} +then : + withval=$with_libssl_lib; SSL_LIB=$withval +else case e in #( + e) SSL_LIB="/usr/lib" ;; +esac +fi + + + +# Check whether --with-libssl-include was given. +if test ${with_libssl_include+y} +then : + withval=$with_libssl_include; SSL_INCLUDE=$withval +else case e in #( + e) SSL_INCLUDE="/usr/include" ;; +esac +fi + + +if test "x$check_libssl" != "xno"; then + LDFLAGS="${LDFLAGS} -L${SSL_LIB} -lssl -lcrypto" + CXXFLAGS="${CXXFLAGS} -I${SSL_INCLUDE} -L${SSL_LIB}" + as_ac_File=`printf "%s\n" "ac_cv_file_"$SSL_LIB/libssl.so"" | sed "$as_sed_sh"` +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for \"$SSL_LIB/libssl.so\"" >&5 +printf %s "checking for \"$SSL_LIB/libssl.so\"... " >&6; } +if eval test \${$as_ac_File+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) test "$cross_compiling" = yes && + as_fn_error $? "cannot check for file existence when cross compiling" "$LINENO" 5 +if test -r ""$SSL_LIB/libssl.so""; then + eval "$as_ac_File=yes" +else + eval "$as_ac_File=no" +fi ;; +esac +fi +eval ac_res=\$$as_ac_File + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5 +printf "%s\n" "$ac_res" >&6; } +if eval test \"x\$"$as_ac_File"\" = x"yes" +then : + +else case e in #( + e) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: Unable to find libssl.so, please use --with-libssl-lib to specify the path" >&5 +printf "%s\n" "$as_me: WARNING: Unable to find libssl.so, please use --with-libssl-lib to specify the path" >&2;} ;; +esac +fi + + as_ac_File=`printf "%s\n" "ac_cv_file_"$SSL_INCLUDE/openssl/ssl.h"" | sed "$as_sed_sh"` +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for \"$SSL_INCLUDE/openssl/ssl.h\"" >&5 +printf %s "checking for \"$SSL_INCLUDE/openssl/ssl.h\"... " >&6; } +if eval test \${$as_ac_File+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) test "$cross_compiling" = yes && + as_fn_error $? "cannot check for file existence when cross compiling" "$LINENO" 5 +if test -r ""$SSL_INCLUDE/openssl/ssl.h""; then + eval "$as_ac_File=yes" +else + eval "$as_ac_File=no" +fi ;; +esac +fi +eval ac_res=\$$as_ac_File + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5 +printf "%s\n" "$ac_res" >&6; } +if eval test \"x\$"$as_ac_File"\" = x"yes" +then : + +else case e in #( + e) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: Unable to find ssl.h, please use --with-libssl-include to specify the path" >&5 +printf "%s\n" "$as_me: WARNING: Unable to find ssl.h, please use --with-libssl-include to specify the path" >&2;} ;; +esac +fi + + +printf "%s\n" "#define HAVE_LIBSSL /**/" >>confdefs.h + +else + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: OpenSSL support disabled (--without-libssl)" >&5 +printf "%s\n" "$as_me: WARNING: OpenSSL support disabled (--without-libssl)" >&2;} +fi + + + # Check whether --with-extra-includes was given. if test ${with_extra_includes+y} then : diff --git a/configure.ac b/configure.ac index b23a6ed3..fa58f995 100644 --- a/configure.ac +++ b/configure.ac @@ -267,6 +267,36 @@ if test "x$check_log4cplus" != "x" ; then CXXFLAGS="${CXXFLAGS} -I${LOG4CPLUS_INCLUDE}" fi +dnl Enable OpenSSL if required + +dnl Enable OpenSSL by default unless --without-libssl is specified +AC_ARG_WITH([libssl], + AS_HELP_STRING([--with-libssl],[Enable OpenSSL (default)]), + [check_libssl=$withval], + [check_libssl=yes]) + +AC_ARG_WITH([libssl-lib], + AS_HELP_STRING([--with-libssl-lib=LIBSSLLIBDIR],[Specify location to find libssl.so]), + [SSL_LIB=$withval], + [SSL_LIB="/usr/lib"]) + +AC_ARG_WITH([libssl-include], + AS_HELP_STRING([--with-libssl-include=SSLINCLUDEDIR],[Specify location to find ssl.h]), + [SSL_INCLUDE=$withval], + [SSL_INCLUDE="/usr/include"]) + +if test "x$check_libssl" != "xno"; then + LDFLAGS="${LDFLAGS} -L${SSL_LIB} -lssl -lcrypto" + CXXFLAGS="${CXXFLAGS} -I${SSL_INCLUDE} -L${SSL_LIB}" + AC_CHECK_FILE("$SSL_LIB/libssl.so",, + [AC_MSG_WARN([Unable to find libssl.so, please use --with-libssl-lib to specify the path])]) + AC_CHECK_FILE("$SSL_INCLUDE/openssl/ssl.h",, + [AC_MSG_WARN([Unable to find ssl.h, please use --with-libssl-include to specify the path])]) + AC_DEFINE([HAVE_LIBSSL], [], [Using LIBSSL]) +else + AC_MSG_WARN([OpenSSL support disabled (--without-libssl)]) +fi + dnl Allow selection of additional includes and libraries paths dnl just in case! diff --git a/doc/cservice.sql b/doc/cservice.sql index f6b0267e..28b93fbb 100644 --- a/doc/cservice.sql +++ b/doc/cservice.sql @@ -3,6 +3,9 @@ -- Channel service DB SQL file for PostgreSQL. -- ChangeLog: +-- 2026-01-09: MrIron +-- Added column for scram records. +-- Added table for TLS fingerprints. -- 2025-04-01: Empus -- Added ident column to user_sec_history table -- Added deleted column to user_sec_history table @@ -221,7 +224,7 @@ CREATE TABLE users ( language_id INT4 CONSTRAINT language_channel_id_ref REFERENCES languages (id), public_key TEXT, post_forms int4 DEFAULT 0 NOT NULL, - flags INT2 NOT NULL DEFAULT '0', + flags INT4 NOT NULL DEFAULT '0', -- 0x00 01 -- Suspended globally. -- 0x00 02 -- Logged in (Depricated). -- 0x00 04 -- Invisible. @@ -241,6 +244,7 @@ CREATE TABLE users ( signup_ts INT4, signup_ip VARCHAR(15), maxlogins INT4 DEFAULT 1, + scram_record TEXT, totp_key VARCHAR(60) DEFAULT '', PRIMARY KEY ( id ) ) ; @@ -250,6 +254,16 @@ CREATE INDEX users_email_idx ON users( lower(email) ); CREATE INDEX users_signup_ts_idx ON users( signup_ts ); CREATE INDEX users_signup_ip_idx ON users( signup_ip ); +-- This table used to store TLS fingerprints. + +CREATE TABLE users_fingerprints ( + user_id INT4 NOT NULL REFERENCES users(id) ON DELETE CASCADE, + fingerprint VARCHAR(128) NOT NULL UNIQUE, + added_ts BIGINT NOT NULL, + added_by VARCHAR(128) NOT NULL, + note TEXT +); + -- This table used to store the "Last Seen" informatation previously -- routinely updated in the users table. CREATE TABLE users_lastseen ( @@ -1104,4 +1118,4 @@ CREATE TABLE default_msgs ( content text NOT NULL ); -CREATE INDEX default_msgs_idx ON default_msgs(type); \ No newline at end of file +CREATE INDEX default_msgs_idx ON default_msgs(type); diff --git a/doc/cservice.update.sql b/doc/cservice.update.sql index b2b8fbcc..3ae697e1 100644 --- a/doc/cservice.update.sql +++ b/doc/cservice.update.sql @@ -99,3 +99,41 @@ $$ LANGUAGE sql STABLE; -- 2025-04-17: Empus -- Added deleted column to languages table ALTER TABLE languages ADD COLUMN deleted INT2 DEFAULT 0; + +-- 2026-01-09: MrIron +-- Added new users_fingerprints table +-- Added new translations and help entries for certauth +-- Added column for scram records. +-- Changed flags column to INT4 to allow for more flags. +-- Added new translations for AUTOHIDE setting. +CREATE TABLE users_fingerprints ( + user_id INT4 CONSTRAINT fingerprints_users_id_ref REFERENCES users ( id ), + fingerprint VARCHAR( 128 ), + added_ts BIGINT NOT NULL, + added_by VARCHAR( 128 ), + note TEXT, + PRIMARY KEY (user_id) +); + +ALTER TABLE users + ADD COLUMN scram_record TEXT, + ALTER COLUMN flags TYPE INT4; + +INSERT INTO translations (language_id, response_id, text, last_updated) VALUES(1, 195, 'Your CERTONLY setting is now ON.', 31337); +INSERT INTO translations (language_id, response_id, text, last_updated) VALUES(1, 196, 'Your CERTONLY setting is now OFF.', 31337); +INSERT INTO translations (language_id, response_id, text, last_updated) VALUES(1, 197, 'Your AUTOHIDE setting is now ON.', 31337); +INSERT INTO translations (language_id, response_id, text, last_updated) VALUES(1, 198, 'Your AUTOHIDE setting is now OFF.', 31337); +INSERT INTO translations (language_id, response_id, text, last_updated) VALUES(1, 225, 'You currently don''t have any fingerprints added to your account. For more information, use ''/msg X help cert''', 31337); +INSERT INTO translations (language_id, response_id, text, last_updated) VALUES(1, 226, 'No fingerprints found.', 31337); +INSERT INTO translations (language_id, response_id, text, last_updated) VALUES(1, 227, 'Your current fingerprint is: %s', 31337); +INSERT INTO translations (language_id, response_id, text, last_updated) VALUES(1, 228, 'You already have %d fingerprints added to your username.', 31337); +INSERT INTO translations (language_id, response_id, text, last_updated) VALUES(1, 229, 'Invalid fingerprint.', 31337); +INSERT INTO translations (language_id, response_id, text, last_updated) VALUES(1, 230, 'That fingerprint is already added to a username.', 31337); +INSERT INTO translations (language_id, response_id, text, last_updated) VALUES(1, 231, 'Successfully added ''%s'' to your username.', 31337); +INSERT INTO translations (language_id, response_id, text, last_updated) VALUES(1, 232, 'Successfully removed ''%s'' from your username.', 31337); +INSERT INTO translations (language_id, response_id, text, last_updated) VALUES(1, 233, 'You cannot remove that fingerprint unless you disable CERTONLY or add another fingerprint.', 31337); +INSERT INTO translations (language_id, response_id, text, last_updated) VALUES(1, 234, 'That fingerprint was not found on your username.', 31337); + +INSERT INTO help VALUES ('CERT', '1', E'/msg X cert [fingerprint] [note]\nThis command allows you to add, remove and list TLS fingerprints added to your username.\nBy connecting to Undernet using TLS and a certificate, you may login to X without using a password if the certificate''s fingerprint has been added to your username.\nTo login to X with a fingerprint, use ''/msg X@channels.undernet.org login [TOTP token]''.\nUsing CERT ADD or REM without specifying a fingerprint will add or remove the fingerprint you are currently connected with (if any).\nA specified fingerprint must be in a SHA-256 format (i.e. AB:CD:12:ED:34 ...).\nWhen having a fingerprint added to your account, you may deactivate password login on IRC using ''SET CERTONLY ON/OFF''. You will still be able to login to the website with your password.'); +INSERT INTO help VALUES ('SET CERTONLY', '1', E'/msg X set CERTONLY \nThis command allows you to enable or disable CERTONLY mode on your username.\nWhen CERTONLY is enabled, you will only be able to login to X using a TLS certificate that has a fingerprint added to your username. Password logins will be disabled while this setting is ON.\nYou must have at least one fingerprint added to your username before enabling CERTONLY.'); +INSERT INTO help VALUES ('SET AUTOHIDE', '1', E'/msg X set AUTOHIDE \nThis command allows you to enable or disable AUTOHIDE mode on your username.\nWhen AUTOHIDE is enabled, you will receive usermode +x automatically upon authenticating with X resulting in your real hostmask being hidden to other users.'); diff --git a/include/Channel.h b/include/Channel.h index cccfb24a..4e7d4a94 100644 --- a/include/Channel.h +++ b/include/Channel.h @@ -129,6 +129,9 @@ class Channel /// Bit representing channel mode +U static const modeType MODE_U ; + /// Bit representing channel mode +Z + static const modeType MODE_Z ; + /// Type used to store number of clients in channel typedef userListType::size_type size_type ; diff --git a/include/Network.h b/include/Network.h index abd29828..4108d5e4 100644 --- a/include/Network.h +++ b/include/Network.h @@ -29,6 +29,7 @@ #include #include #include +#include #include @@ -141,6 +142,12 @@ class xNetwork */ typedef std::map< unsigned int, iServer* > serverMapType ; + /** + * The type used to store Network configuration pairs (CF), + * keyed by the configuration key. + */ + typedef std::map< std::string, std::pair< std::string, time_t > > netConfMapType ; + public: /** @@ -205,6 +212,20 @@ class xNetwork */ virtual bool addChannel( Channel* ) ; + /** + * Add a new netconf variable to the network table. + */ + virtual void addNetConf( const std::string& key, + const std::string& value, + time_t timestamp ) + { netConfMap[ key ] = std::make_pair( value, timestamp ) ; } + + /** + * Find a netconf variable by key. + * Returns std::nullopt if not found. + */ + virtual std::optional< std::pair< std::string, time_t > > findNetConf( const std::string& key ) const ; + /* * All nickname based searches are case insensitive. */ @@ -972,6 +993,12 @@ class xNetwork */ serverMapType serverMap ; + /** + * This structure holds all network configuration pairs + * bursted on the network (CF). + */ + netConfMapType netConfMap ; + /** * This variable is used backwards calls to the main server. */ diff --git a/include/defs.h.in b/include/defs.h.in index 5a12477c..e5367842 100644 --- a/include/defs.h.in +++ b/include/defs.h.in @@ -100,6 +100,9 @@ /* Define to 1 if you have the 'socket' library (-lsocket). */ #undef HAVE_LIBSOCKET +/* Using LIBSSL */ +#undef HAVE_LIBSSL + /* Define this if a modern libltdl is already installed */ #undef HAVE_LTDL diff --git a/include/events.h b/include/events.h index 41304e9a..d6979005 100644 --- a/include/events.h +++ b/include/events.h @@ -59,6 +59,7 @@ enum EVT_RAW, EVT_XQUERY, EVT_XREPLY, + EVT_NETCONF, // EVT_NOOP must always be last EVT_NOOP @@ -127,6 +128,9 @@ typedef int channelEventType ; * 1) iServer* source * 2) string* - routing * 3) string* - command + * EVT_NETCONF + * 1) iServer* source + * 2) string* - key * * Channel Events * -------------- diff --git a/include/gnuworld_config.h b/include/gnuworld_config.h index 9c86bda3..f40486d1 100755 --- a/include/gnuworld_config.h +++ b/include/gnuworld_config.h @@ -81,6 +81,13 @@ */ #define TOPIC_TRACK +/** + * NO_FINGERPRINT_BURST + * Disable this if gnuworld is linked to a hub that doesn't support fingerprint burst. + * This should only be enabled when the uplink is running ircu with tls support. + */ +#define NO_FINGERPRINT_BURST + /** * USE_THREAD * Set this to enable mutexes on shared objects. Relevant for running diff --git a/include/iClient.h b/include/iClient.h index 1c407723..e5a301c0 100644 --- a/include/iClient.h +++ b/include/iClient.h @@ -97,6 +97,9 @@ class iClient : public NetworkTarget /** MODE_FAKE is true if this user is a fake client. */ static constexpr modeType MODE_FAKE = 0x0200 ; + /** MODE_TLS is true if this user is using TLS. */ + static constexpr modeType MODE_TLS = 0x0400 ; + /** * Define a type to be used for storing the * iClient's account's flags. @@ -106,6 +109,9 @@ class iClient : public NetworkTarget static constexpr flagType X_TOTP_REQ_IPR = 0x002 ; static constexpr flagType X_GLOBAL_SUSPEND = 0x004 ; static constexpr flagType X_FRAUD = 0x008 ; + static constexpr flagType X_CERTONLY = 0x010 ; + static constexpr flagType X_CERT_DISABLE_TOTP = 0x020 ; + static constexpr flagType X_WEB_DISABLE_TOTP = 0x040 ; /// Iterator for channels this user is on. typedef channelListType::iterator channelIterator ; @@ -131,6 +137,7 @@ class iClient : public NetworkTarget const std::string& _account, const unsigned int _account_id, const flagType _account_flags, + const std::string& _tls_fingerprint, const std::string& _description, const time_t& _nick_ts ) ; @@ -151,6 +158,7 @@ class iClient : public NetworkTarget const std::string& _account, const unsigned int _account_id, const flagType _account_flags, + const std::string& _tls_fingerprint, const std::string& _setHost, const std::string& _fakeHost, const std::string& _description, @@ -213,6 +221,19 @@ class iClient : public NetworkTarget inline const std::string getRealNickUserHost() const { return (nickName + '!' + userName + '@' + realInsecureHost) ; } + /** + * Retrieve client's TLS fingerprint. + * Will return empty if the client is not using TLS. + */ + inline const std::string& getTlsFingerprint() const + { return tlsFingerprint ; } + + /** + * Returns true if the client has a fingerprint. + */ + inline bool hasTlsFingerprint() const + { return !tlsFingerprint.empty() ; } + /** * Retrieve client's 'real-name' field. */ @@ -309,6 +330,15 @@ class iClient : public NetworkTarget account_flags |= newFlag ; } + /** + * Set the TLS fingerprint for this iClient. + */ + inline void setTlsFingerprint( const std::string& _tls_fingerprint ) + { + if( ! isModeZ() ) { return ; } + tlsFingerprint = _tls_fingerprint ; + } + #ifdef ASUKA /** * Retrieve the iClient's sethost. @@ -498,6 +528,13 @@ class iClient : public NetworkTarget inline bool isModeG() const { return getMode( MODE_G ) ; } + /** + * Return true if this client has the +z mode set, + * indicating that the client is using TLS. False otherwise. + */ + inline bool isModeZ() const + { return getMode( MODE_TLS ) ; } + /** * Return true if this iClient is a fake, false otherwise. */ @@ -573,6 +610,12 @@ class iClient : public NetworkTarget inline void setModeR() { setMode( MODE_REGISTERED ) ; } + /** + * Set mode +z for this user. + */ + inline void setModeZ() + { setMode( MODE_TLS ) ; } + /** * Designate this iClient as a fake. */ @@ -766,7 +809,10 @@ class iClient : public NetworkTarget unsigned int account_id ; /** The flags of this client's account. */ - unsigned short int account_flags; + unsigned short int account_flags ; + + /** The TLS fingerprint of this client. */ + std::string tlsFingerprint ; #ifdef ASUKA /** diff --git a/include/server.h b/include/server.h index 19d99b7d..76d33078 100644 --- a/include/server.h +++ b/include/server.h @@ -1294,6 +1294,12 @@ class xServer : public ConnectionManager, */ long Version ; + /** + * Whether or not TLS encrypted connections are enabled (and required) + * for the server uplink + */ + bool tlsEnabled ; + /** * This variable is true when this server is bursting. */ @@ -1522,6 +1528,16 @@ class xServer : public ConnectionManager, */ std::string simFileName ; + /** + * The path to the TLS key file + */ + std::string tlsKeyFile ; + + /** + * The path to the TLS cert file + */ + std::string tlsCertFile ; + /** * True if autoreconnect is enabled, false otherwise. */ @@ -1543,6 +1559,11 @@ class xServer : public ConnectionManager, bool loadCommandHandlers() ; + /** + * This method initializes all of the TLS contexts + */ + bool initTls() ; + /** * Load an individual command handler from a file (fileName), * and associate that handler with the network message diff --git a/libgnuworld/Connection.cc b/libgnuworld/Connection.cc index 4ef47654..bbc057f5 100755 --- a/libgnuworld/Connection.cc +++ b/libgnuworld/Connection.cc @@ -34,31 +34,28 @@ namespace gnuworld { -// Allocate these static variables in class Connection -const Connection::flagType Connection::F_PENDING = 0x01 ; -const Connection::flagType Connection::F_CONNECTED = 0x02 ; -const Connection::flagType Connection::F_INCOMING = 0x04 ; -const Connection::flagType Connection::F_LISTEN = 0x08 ; -const Connection::flagType Connection::F_FILE = 0x10 ; -const Connection::flagType Connection::F_FLUSH = 0x20 ; - using std::string ; // Simply initialize the object Connection::Connection( const string& _hostname, const unsigned short int _remotePort, - const char _delimiter ) + const char _delimiter, + bool _tlsEnabled ) : hostname( _hostname ), localPort( 0 ), remotePort( _remotePort ), inputBuffer( _delimiter ), outputBuffer( _delimiter ), + tlsEnabled( _tlsEnabled ), IP( string() ), sockFD( -1 ), flags( F_PENDING ), connectTime( 0 ), bytesRead( 0 ), bytesWritten( 0 ) +#ifdef HAVE_LIBSSL + , tlsState( nullptr ) +#endif { memset( &addr, 0, sizeof( struct sockaddr_in ) ) ; } @@ -68,15 +65,28 @@ Connection::Connection( const char _delimiter ) remotePort( 0 ), inputBuffer( _delimiter ), outputBuffer( _delimiter ), + tlsEnabled( false ), sockFD( -1 ), - flags( F_PENDING ) + flags( F_PENDING ), + connectTime( 0 ), + bytesRead( 0 ), + bytesWritten( 0 ) +#ifdef HAVE_LIBSSL + , tlsState( nullptr ) +#endif { memset( &addr, 0, sizeof( struct sockaddr_in ) ) ; } Connection::~Connection() { -/* No work to be done, no heap space allocated */ +#ifdef HAVE_LIBSSL +if( tlsState ) + { + SSL_free( tlsState ) ; + tlsState = nullptr ; + } +#endif } void Connection::Write( const string& writeMe ) diff --git a/libgnuworld/Connection.h b/libgnuworld/Connection.h index 103b18ff..fa01955a 100755 --- a/libgnuworld/Connection.h +++ b/libgnuworld/Connection.h @@ -35,6 +35,13 @@ #include "Buffer.h" #include "ELog.h" +#include "defs.h" + +#ifdef HAVE_LIBSSL +#include +#include +#include +#endif namespace gnuworld { @@ -102,13 +109,13 @@ class Connection * This flag is true if the connection is currently fully * connected. */ - static const flagType F_CONNECTED ; + static constexpr flagType F_CONNECTED = 0x01 ; /** * This flag is true if the connection is still pending, * not yet complete. */ - static const flagType F_PENDING ; + static constexpr flagType F_PENDING = 0x02 ; /** * This flag is true if this Connection represents a @@ -117,7 +124,7 @@ class Connection * The F_INCOMING flag exists throughout the life ot * the Connection. */ - static const flagType F_INCOMING ; + static constexpr flagType F_INCOMING = 0x04 ; /** * This flag is true if this Connection represents a @@ -125,20 +132,36 @@ class Connection * This flag exists for the life of this connection. * A listening Connection is never connected. */ - static const flagType F_LISTEN ; + static constexpr flagType F_LISTEN = 0x08 ; /** * This flag is true when the Connection represents a * connection to a file, rather than a network connection. */ - static const flagType F_FILE ; + static constexpr flagType F_FILE = 0x10 ; /** * This flag will be set if the next write is to flush all * data from the output buffer. * The flag will be reset after the send(). */ - static const flagType F_FLUSH ; + static constexpr flagType F_FLUSH = 0x20 ; + + /** + * This flag will be set upon initiating a TLS connection. + * The flag will be reset after the handshake is completed. + */ + static constexpr flagType F_TLS_HANDSHAKING = 0x40 ; + + /** + * This flag will be set upon shutting down a TLS connection. + */ + static constexpr flagType F_TLS_SHUTTING_DOWN = 0x80 ; + + /** + * This flags a TLS connection as having a fatal error, i.e. ssl_shutdown() will not be called. + */ + static constexpr flagType F_TLS_FATAL_ERROR = 0x100 ; /** * Append a string to the Connection's output buffer. @@ -207,6 +230,19 @@ class Connection inline bool isFlush() const { return (flags & F_FLUSH) ; } + /** + * Return true if the negotiating TLS flag is set, indicating that + * the TLS handshake has not completed. + */ + inline bool isNegotiatingTLS() const + { return (flags & F_TLS_HANDSHAKING) ; } + + /** + * Return true if the TLS connection has initiating shutdown. + */ + inline bool isShuttingDownTLS() const + { return (flags & F_TLS_SHUTTING_DOWN) ; } + /** * Return the total number of bytes read from this * Connection. @@ -242,6 +278,12 @@ class Connection */ inline size_t getInputBufferSize() const { return inputBuffer.size() ; } + + /** + * Return whether or not this connection has TLS enabled + */ + inline bool isTLS() const + { return tlsEnabled ; } /** * Set the F_FLUSH flag to true. @@ -261,6 +303,7 @@ class Connection << ", localPort: " << con.getLocalPort() << ", remotePort: " << con.getRemotePort() << ", sockFD: " << con.getSockFD() + << ", TLS: " << (con.isTLS() ? "yes" : "no") << ", state: " << (con.isConnected() ? "connected" : "pending") ; if( con.isIncoming() ) @@ -285,6 +328,7 @@ class Connection << ", localPort: " << con.getLocalPort() << ", remotePort: " << con.getRemotePort() << ", sockFD: " << con.getSockFD() + << ", TLS: " << (con.isTLS() ? "yes" : "no") << ", state: " << (con.isConnected() ? "connected" : "pending") ; if( con.isIncoming() ) @@ -306,7 +350,8 @@ class Connection */ Connection( const std::string& host, const unsigned short int remotePort, - const char delimiter ) ; + const char delimiter, + bool tlsEnabled ) ; /** * Create a new instance of this class, set all variables @@ -358,6 +403,24 @@ class Connection inline void setFile() { setFlag( F_FILE ) ; } + /** + * Mark that this Connection as pending TLS negotiation. + */ + inline void setNegotiatingTLS() + { setFlag( F_TLS_HANDSHAKING ) ; } + + /** + * Mark that this Connection as pending TLS negotiation. + */ + inline void clrNegotiatingTLS() + { removeFlag( F_TLS_HANDSHAKING ) ; } + + /** + * Mark that this Connection as pending TLS negotiation. + */ + inline void setShuttingDownTLS() + { setFlag( F_TLS_SHUTTING_DOWN ) ; } + /** * Set this connection's local port number */ @@ -414,6 +477,17 @@ class Connection */ inline time_t getAbsTimeout() const { return absTimeout ; } + +#ifdef HAVE_LIBSSL + /** + * Return the current TLS state for this connection + */ + inline SSL* getTlsState() const + { return tlsState ; } + + inline void setTlsState( SSL* newState ) + { tlsState = newState ; } +#endif /** * The remote hostname of this connection @@ -441,6 +515,11 @@ class Connection */ Buffer outputBuffer ; + /** + * Whether or not this connection is encrypted + */ + bool tlsEnabled ; + /** * The remote IP of this connection * This variable is empty() if this Connection is a listener @@ -484,6 +563,12 @@ class Connection */ size_t bytesWritten ; +#ifdef HAVE_LIBSSL + /** + * The current OpenSSL TLS state for this connection + */ + SSL* tlsState ; +#endif } ; } // namespace gnuworld diff --git a/libgnuworld/ConnectionManager.cc b/libgnuworld/ConnectionManager.cc index 62ed33e1..85397302 100755 --- a/libgnuworld/ConnectionManager.cc +++ b/libgnuworld/ConnectionManager.cc @@ -86,7 +86,7 @@ eraseMap.clear() ; for( handlerMapIterator handlerItr = handlerMap.begin() ; handlerItr != handlerMap.end() ; ++handlerItr ) { - for( connectionMapIterator connectionItr = + for( connectionMapIterator connectionItr = handlerItr->second.begin() ; connectionItr != handlerItr->second.end() ; ++connectionItr ) @@ -109,7 +109,8 @@ handlerMap.clear() ; Connection* ConnectionManager::Connect( ConnectionHandler* hPtr, const string& host, - const unsigned short int remotePort ) + const unsigned short int remotePort, + bool tlsEnabled = false ) { // Handler must be valid assert( hPtr != 0 ) ; @@ -128,7 +129,7 @@ if( host.empty() ) // Allocate a new Connection object Connection* newConnection = new (nothrow) - Connection( host, remotePort, delimiter ) ; + Connection( host, remotePort, delimiter, tlsEnabled ) ; assert( newConnection != 0 ) ; // Set the absolute time for this Connection's timeout to occur @@ -184,6 +185,22 @@ addr->sin_addr.s_addr = inet_addr( newConnection->getIP().c_str() ) ; // Update the new Connection object newConnection->setSockFD( sockFD ) ; +#ifdef HAVE_LIBSSL +if (tlsEnabled) { + SSL* state = SSL_new(sslCtx); + if (!state) { + elog << "Connect> Could not create SSL session" << endl; + return 0; + } + newConnection->setTlsState(state); + newConnection->setNegotiatingTLS(); + SSL_set_fd(state, sockFD); + BIO_set_nbio(SSL_get_rbio(state), 1); + BIO_set_nbio(SSL_get_wbio(state), 1); + SSL_set_connect_state(state); +} +#endif + // Attempt to initiate the connect. // The socket is non-blocking, so a failure is expected // In the case of a UDP socket, this call to ::connect() will just @@ -551,6 +568,24 @@ elog << "ConnectionManager::Disconnect> Scheduling connection " // to Poll() scheduleErasure( hPtr, connectionItr ) ; +// Flush buffer. +cPtr->Flush() ; + +#ifdef HAVE_LIBSSL +// Attempt to shutdown TLS connection. +if( cPtr->isTLS() && !cPtr->isNegotiatingTLS() && !cPtr->hasFlag( Connection::F_TLS_FATAL_ERROR ) ) + { + // Flag the connections as shutting down. + cPtr->setShuttingDownTLS() ; + + int res = SSL_shutdown( cPtr->getTlsState() ) ; + if( res > 0 ) + { + elog << "ConnectionManager::Disconnect> TLS connection gracefully shut down." << endl ; + } + } +#endif + // Connection located and scheduled for erasure, return success return true ; } @@ -868,6 +903,24 @@ for( eraseMapIterator eraseItr = eraseMap.begin(), // << *connectionPtr // << endl ; +#ifdef HAVE_LIBSSL + // Attempt to shutdown TLS connection if not already closed. + if( connectionPtr->isTLS() && !connectionPtr->isNegotiatingTLS() && !connectionPtr->hasFlag( Connection::F_TLS_FATAL_ERROR )) + { + int res = SSL_shutdown( connectionPtr->getTlsState() ) ; + if( res > 0 ) + { + elog << "ConnectionManager::Poll> TLS connection gracefully shut down." << endl ; + } + else + { + elog << "ConnectionManager::Poll> TLS failed to shut down gracefully (" << res << "): " + << ERR_error_string( ERR_get_error(), nullptr ) + << endl ; + } + } +#endif + // Close the Connection's socket (file) descriptor closeSocket( connectionPtr->getSockFD() ) ; @@ -891,11 +944,11 @@ for( handlerMapIterator handlerItr = handlerMap.begin() ; handlerItr != handlerMap.end() ; ++handlerItr ) { // From SGI STL website: - // Map has the important property that inserting a new element - // into a map does not invalidate iterators that point to + // Map has the important property that inserting a new element + // into a map does not invalidate iterators that point to // existing elements. Erasing an element from a map also does - // not invalidate any iterators, except, of course, for iterators - // that actually point to the element that is being erased. + // not invalidate any iterators, except, of course, for iterators + // that actually point to the element that is being erased. if( handlerItr->second.empty() ) { // The connectionMap for this handler is empty @@ -1010,6 +1063,17 @@ ::close( fd ) ; bool ConnectionManager::handleRead( ConnectionHandler* hPtr, Connection* cPtr ) { +// Don't allow reads if the connection is shutting down. +if( cPtr->isTLS() && cPtr->isShuttingDownTLS() ) + return true ; + +#ifdef HAVE_LIBSSL +// Don't allow reads until TLS handshake is complete +if( cPtr->isTLS() && cPtr->isNegotiatingTLS() ) + { + return negotiateTLS( hPtr, cPtr ) ; + } +#endif // protected member, no error checking // Attempt the read from the socket @@ -1026,8 +1090,40 @@ if( cPtr->isFile() ) else { // Network connection - readResult = ::recv( cPtr->getSockFD(), inputBuffer, - inputBufferSize, 0 ) ; + if( cPtr->isTLS() ) + { +#ifdef HAVE_LIBSSL + readResult = SSL_read( cPtr->getTlsState(), inputBuffer, inputBufferSize ) ; + if( readResult <= 0 ) + { + int err = SSL_get_error( cPtr->getTlsState(), readResult ) ; + switch( err ) + { + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_WRITE: + // Not ready - try again later + return true ; + + case SSL_ERROR_ZERO_RETURN: + // Clean shutdown + hPtr->OnDisconnect( cPtr ) ; + return false ; + + default: + elog << "ConnectionManager::handleRead> TLS read error: " + << ERR_error_string( ERR_get_error(), nullptr ) << endl ; + cPtr->setFlag( Connection::F_TLS_FATAL_ERROR ) ; + hPtr->OnDisconnect( cPtr ) ; + return false ; + } + } +#endif + } + else + { + readResult = ::recv( cPtr->getSockFD(), inputBuffer, + inputBufferSize, 0 ) ; + } } if( EAGAIN == errno ) @@ -1082,6 +1178,18 @@ return true ; bool ConnectionManager::handleWrite( ConnectionHandler* hPtr, Connection* cPtr ) { +// Don't allow reads if the connection is shutting down. +if( cPtr->isTLS() && cPtr->isShuttingDownTLS() ) + return true ; + +#ifdef HAVE_LIBSSL +// Don't allow writes until TLS handshake is complete +if( cPtr->isTLS() && cPtr->isNegotiatingTLS() ) + { + return negotiateTLS( hPtr, cPtr ) ; + } +#endif + // protected member, no error checking // Attempt the write to the socket @@ -1099,7 +1207,6 @@ if( cPtr->isFile() ) { // Just ignore writes to the file cPtr->outputBuffer.clear() ; - return true ; } @@ -1109,16 +1216,45 @@ if( cPtr->isFlush() ) } errno = 0 ; -int writeResult = ::send( cPtr->getSockFD(), - cPtr->outputBuffer.data(), - cPtr->outputBuffer.size(), - 0 ) ; +int writeResult = 0 ; +if ( !cPtr->isTLS() ) { + writeResult = ::send( cPtr->getSockFD(), + cPtr->outputBuffer.data(), + cPtr->outputBuffer.size(), + 0 ) ; +} else { +#ifdef HAVE_LIBSSL + writeResult = SSL_write( cPtr->getTlsState(), + cPtr->outputBuffer.data(), + cPtr->outputBuffer.size() ) ; + if( writeResult < 0 ) + { + int err = SSL_get_error( cPtr->getTlsState(), writeResult) ; + switch( err ) + { + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_WRITE: + return true ; /* Retry when the socket is ready. */ + case SSL_ERROR_ZERO_RETURN: + // Clean shutdown + hPtr->OnDisconnect( cPtr ) ; + return false ; + default: + elog << "ConnectionManager::handleWrite> TLS write error: " + << ERR_error_string( ERR_get_error(), nullptr ) << endl ; + cPtr->setFlag( Connection::F_TLS_FATAL_ERROR ) ; + hPtr->OnDisconnect( cPtr ) ; + return false ; + } + } +#endif +} if( (ENOBUFS == errno) || (EWOULDBLOCK == errno) || (EAGAIN == errno) ) { // Nonblocking type error // Ignore it for now - elog << "ConnectionManager::handleWrite> errno: " + elog << "ConnectionManager::handleWrite> errno: (" << errno << ") " << strerror( errno ) << endl ; return true ; @@ -1159,6 +1295,12 @@ bool ConnectionManager::handleFlush( ConnectionHandler* hPtr, { // protected member, no error checking +#ifdef HAVE_LIBSSL +// This function returns false if TLS handshake negotiations are not completed. +if( !negotiateTLS( hPtr, cPtr ) ) + return false ; +#endif + // Make sure the Connection's F_FLUSH flag is cleared, so do it first cPtr->removeFlag( Connection::F_FLUSH ) ; @@ -1197,10 +1339,40 @@ if( ::fcntl( cPtr->getSockFD(), F_SETFL, flags ) < 0 ) while( !cPtr->outputBuffer.empty() ) { errno = 0 ; - int writeResult = ::send( cPtr->getSockFD(), - cPtr->outputBuffer.data(), - cPtr->outputBuffer.size(), - 0 ) ; + int writeResult = 0 ; + if( !cPtr->isTLS()) + writeResult = ::send( cPtr->getSockFD(), + cPtr->outputBuffer.data(), + cPtr->outputBuffer.size(), + 0 ) ; + #ifdef HAVE_LIBSSL + else + { + writeResult = SSL_write( cPtr->getTlsState(), + cPtr->outputBuffer.data(), + cPtr->outputBuffer.size() ) ; + if( writeResult < 0 ) + { + int err = SSL_get_error( cPtr->getTlsState(), writeResult ) ; + + switch( err ) + { + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_WRITE: + return true ; // Retry when the socket is ready. + case SSL_ERROR_ZERO_RETURN: + // Clean shutdown + hPtr->OnDisconnect( cPtr ) ; + return false ; + default: + elog << "ConnectionManager::HandleFlush> Fatal TLS error encountered." << endl ; + cPtr->setFlag( Connection::F_TLS_FATAL_ERROR ) ; + hPtr->OnDisconnect( cPtr ) ; + return false ; + } + } + } + #endif if( (ENOBUFS == errno) || (EWOULDBLOCK == errno) || (EAGAIN == errno) ) { @@ -1677,7 +1849,7 @@ if( fd < 0 ) // Create a new Connection object to represent this open file Connection* newConnect = new (std::nothrow) - Connection( fileName, fd, delimiter ) ; + Connection( fileName, fd, delimiter, false ) ; assert( newConnect != 0 ) ; // Set the Connection's state @@ -1733,4 +1905,50 @@ if( hItr == handlerMap.end() ) return hItr->second.size() ; } +#ifdef HAVE_LIBSSL +bool ConnectionManager::negotiateTLS( ConnectionHandler* hPtr, Connection* cPtr ) +{ +/* Return true if negotiations are completed. */ +if( !cPtr->isNegotiatingTLS() ) + return true ; + +if( ::time( 0 ) > ( cPtr->connectTime + 5 ) ) + { + elog << "ConnectionManager::negotiateTLS> TLS handshake timed out" << endl ; + hPtr->OnDisconnect( cPtr ) ; + return false ; + } + +int res = SSL_connect( cPtr->getTlsState() ) ; +if( res == 1 ) + { + elog << "ConnectionManager::negotiateTLS> TLS handshake completed successfully." << endl ; + cPtr->clrNegotiatingTLS() ; + return true ; + } + +int err = SSL_get_error( cPtr->getTlsState(), res ) ; +switch( err ) + { + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_WRITE: + // This is normal for non-blocking - return true to continue in next poll + return true ; + + case SSL_ERROR_ZERO_RETURN: + // Clean shutdown + elog << "ConnectionManager::negotiateTLS> TLS connection closed cleanly" << endl ; + hPtr->OnDisconnect( cPtr ) ; + return false ; + + default: + // Fatal error + elog << "ConnectionManager::negotiateTLS> TLS handshake failed: " + << ERR_error_string( ERR_get_error(), nullptr ) << endl ; + cPtr->setFlag( Connection::F_TLS_FATAL_ERROR ) ; + hPtr->OnDisconnect( cPtr ) ; + return false ; + } +} +#endif } // namespace gnuworld diff --git a/libgnuworld/ConnectionManager.h b/libgnuworld/ConnectionManager.h index aa280edb..c9987b55 100755 --- a/libgnuworld/ConnectionManager.h +++ b/libgnuworld/ConnectionManager.h @@ -37,6 +37,12 @@ #include "Connection.h" #include "ConnectionHandler.h" +#ifdef HAVE_LIBSSL +#include +#include +#include +#endif + namespace gnuworld { @@ -188,7 +194,8 @@ class ConnectionManager virtual Connection* Connect( ConnectionHandler*, const std::string& host, - const unsigned short int remotePort ) ; + const unsigned short int remotePort, + bool tlsEnabled ) ; /** * Connect to a file instead of a network host. This method @@ -334,6 +341,13 @@ class ConnectionManager */ char* inputBuffer ; +#ifdef HAVE_LIBSSL + /** + * The OpenSSL context for TLS connections + */ + SSL_CTX* sslCtx ; +#endif + /** * Open a socket. * Return -1 on error @@ -421,6 +435,9 @@ class ConnectionManager */ virtual bool disconnectAll( ConnectionHandler* ) ; +#ifdef HAVE_LIBSSL + virtual bool negotiateTLS( ConnectionHandler*, Connection* ) ; +#endif } ; } // namespace gnuworld diff --git a/libgnuworld/misc.cc b/libgnuworld/misc.cc index 19f30eb4..7dbca1be 100644 --- a/libgnuworld/misc.cc +++ b/libgnuworld/misc.cc @@ -34,6 +34,7 @@ #include #include #include +#include #include "misc.h" #include "StringTokenizer.h" @@ -693,6 +694,40 @@ getrusage( RUSAGE_SELF, &usage ) ; return usage.ru_maxrss ; } +// Converts "12asb23b..." to "12:AS:B2:3B..." +std::string compactToCanonical( const std::string& fingerprint ) +{ +std::string result ; +for( size_t i = 0 ; i < fingerprint.size() ; ++i ) + { + result += std::toupper( fingerprint[ i ] ) ; + if( ( i + 1 ) % 2 == 0 && i + 1 < fingerprint.size() ) + result += ':' ; +} +return result ; +} + +// Converts "12:AS:B2:3B..." back to "12asb23b..." +std::string canonicalToCompact( const std::string& fingerprint ) +{ +std::string result ; +for( char c : fingerprint ) + if( c != ':' ) + result += std::tolower( c ) ; +return result ; +} + +// SHA-256 fingerprint format: 32-byte hash in uppercase hex, separated by colons +bool isValidSHA256Fingerprint( const std::string& fingerprint ) +{ +const std::regex fingerprintRegex( "^([A-F0-9]{2}:){31}[A-F0-9]{2}$" ) ; + +if( fingerprint.length() != 95 ) + return false ; + +return std::regex_match( fingerprint, fingerprintRegex ) ; +} + /* Returns the CPU time used by gnuworld in seconds. */ double getCPUTime() { struct rusage usage ; diff --git a/libgnuworld/misc.h b/libgnuworld/misc.h index 737543e1..6a18c2c4 100755 --- a/libgnuworld/misc.h +++ b/libgnuworld/misc.h @@ -245,6 +245,13 @@ void stripModes( std::string& , const std::string& ) ; /* Returns the memory usage of gnuworld in KB. */ size_t getMemoryUsage() ; +std::string compactToCanonical( const std::string& ) ; + +// Converts "12:AS:B2:3B..." back to "12asb23b..." +std::string canonicalToCompact( const std::string& ) ; + +bool isValidSHA256Fingerprint( const std::string& ) ; + /* Returns the CPU time used by gnuworld in seconds. */ double getCPUTime() ; diff --git a/libircu/Makefile.am b/libircu/Makefile.am index 091fe040..99f70c42 100644 --- a/libircu/Makefile.am +++ b/libircu/Makefile.am @@ -7,6 +7,7 @@ libircu_la_SOURCES = \ libircu/msg_AD.cc \ libircu/msg_B.cc \ libircu/msg_C.cc \ + libircu/msg_CF.cc \ libircu/msg_CM.cc \ libircu/msg_D.cc \ libircu/msg_DS.cc \ diff --git a/libircu/msg_B.cc b/libircu/msg_B.cc index be757f18..2360152b 100644 --- a/libircu/msg_B.cc +++ b/libircu/msg_B.cc @@ -237,6 +237,10 @@ if( '+' == Param[ whichToken ][ 0 ] ) modeVector.push_back( make_pair( true, Channel::MODE_MNOREG ) ) ; break ; + case 'Z': + modeVector.push_back( make_pair( + true, Channel::MODE_Z ) ) ; + break ; case 'l': theServer->OnChannelModeL( theChan, true, 0, diff --git a/libircu/msg_CF.cc b/libircu/msg_CF.cc new file mode 100755 index 00000000..aa2cc091 --- /dev/null +++ b/libircu/msg_CF.cc @@ -0,0 +1,92 @@ +/** + * msg_CF.cc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + * + */ + + #include + #include + + #include "gnuworld_config.h" + #include "ServerCommandHandler.h" + #include "server.h" + #include "xparameters.h" + #include "Channel.h" + #include "Network.h" + #include "iClient.h" + #include "ELog.h" + + namespace gnuworld + { + + CREATE_HANDLER(msg_CF) + + /** + * CONFIGURATION message handler. + * + * This message is used to propagate network configuration + * source CF : + */ + bool msg_CF::Execute( const xParameters& Param ) + { + if( Param.size() < 3 ) + { + elog << "msg_CF> Invalid number of parameters" + << std::endl ; + return false ; + } + + iServer* sourceServer = Network->findServer( Param[ 0 ] ) ; + if( NULL == sourceServer ) + { + elog << "msg_CF> Unable to find source server: " + << Param[ 0 ] << std::endl ; + return false ; + } + +time_t timestamp = atoi( Param[ 1 ] ) ; +std::string key( Param[ 2 ] ) ; +std::string value( Param.assemble( 3 ) ) ; + +auto netConf = Network->findNetConf( key ) ; +if( netConf && netConf->second > timestamp ) + { + // This should never happen. + elog << "msg_CF> Netconf variable already exists and is newer: " + << key + << " (timestamp on file: " << netConf->second << ", timestamp on network: " << timestamp << ")" + << std::endl ; + return false ; + } + + Network->addNetConf( key, value, timestamp ) ; + + elog << "msg_CF> Adding netconf variable: " + << key + << " (value: " << value << ")" + << " (timestamp: " << timestamp << ")" + << std::endl ; + + // Post event to listening clients + theServer->PostEvent( EVT_NETCONF, static_cast< void* >( sourceServer ), + static_cast< void* >( &key ) ) ; + + // Return success + return true ; + } + + } // namespace gnuworld diff --git a/libircu/msg_CM.cc b/libircu/msg_CM.cc index d19e02ea..92d4c83e 100644 --- a/libircu/msg_CM.cc +++ b/libircu/msg_CM.cc @@ -196,6 +196,13 @@ for( std::string::size_type i = 0 ; i < Modes.size() ; i++ ) false, Channel::MODE_MNOREG ) ) ; // elog << tmpChan->getName() // << "msg_CM> Doing CLEAR_MNOREG" +// << endl; + break ; + case 'Z': + modeVector.push_back( make_pair( + false, Channel::MODE_Z ) ) ; +// elog << tmpChan->getName() +// << "msg_CM> Doing CLEAR_Z" // << endl; break ; case 'k': diff --git a/libircu/msg_M.cc b/libircu/msg_M.cc index 7f78b122..a4ab6488 100644 --- a/libircu/msg_M.cc +++ b/libircu/msg_M.cc @@ -232,6 +232,10 @@ for( const char* modePtr = Param[ 2 ] ; *modePtr ; ++modePtr ) modeVector.push_back( make_pair(polarity, Channel::MODE_MNOREG)); break; + case 'Z': + modeVector.push_back( + make_pair(polarity, Channel::MODE_Z)); + break; // Channel mode l only has an argument if // it is being added, but not removed case 'l': diff --git a/libircu/msg_N.cc b/libircu/msg_N.cc index fd94dbb9..cd0ba7bb 100644 --- a/libircu/msg_N.cc +++ b/libircu/msg_N.cc @@ -162,6 +162,7 @@ unsigned int account_id = 0 ; unsigned short int account_flags = 0; string sethost ; string fakehost ; +string tlsFingerprint ; xParameters::size_type currentArgIndex = 6 ; @@ -190,6 +191,11 @@ if( '+' == params[ currentArgIndex ][ 0 ] ) case 'f': fakehost = params[ currentArgIndex++ ] ; break ; +#ifndef NO_FINGERPRINT_BURST + case 'z': + tlsFingerprint = params[ currentArgIndex++ ] ; + break ; +#endif default: break ; } // switch( *modePtr ) } // for() @@ -229,6 +235,10 @@ if( !account.empty() ) } // if( 3 == st.size() ) } // if( !account.empty() ) +/* Set the fingerprint to empty if it is empty. */ +if( tlsFingerprint == "_" ) + tlsFingerprint.clear() ; + /* * -3 * -2 @@ -250,6 +260,7 @@ iClient* newClient = new (std::nothrow) iClient( account, // account account_id, // account id account_flags, // account flags + tlsFingerprint, // TLS fingerprint sethost, // asuka sethost fakehost, // srvx fakehost description, // real name / infoline diff --git a/libltdl/configure b/libltdl/configure old mode 100755 new mode 100644 diff --git a/mod.ccontrol/CLEARCHANCommand.cc b/mod.ccontrol/CLEARCHANCommand.cc index 8a8fc1d3..46b855f8 100644 --- a/mod.ccontrol/CLEARCHANCommand.cc +++ b/mod.ccontrol/CLEARCHANCommand.cc @@ -88,9 +88,9 @@ if(Chan) //Check if the user specified the modes, if not assume he ment all of the modes if(st.size() == 2) - doModes = "obklimrD"; + doModes = "obklimrDZ"; else if(!strcasecmp(string_upper(st[ 2 ]),"ALL")) - doModes = "obklimnsptrDCc"; + doModes = "obklimnsptrDCcuMZ"; else if(!strcasecmp(string_upper(st [ 2]),"-d")) Desynch = true; else diff --git a/mod.ccontrol/WHOISCommand.cc b/mod.ccontrol/WHOISCommand.cc index 62061f64..b3662ce4 100644 --- a/mod.ccontrol/WHOISCommand.cc +++ b/mod.ccontrol/WHOISCommand.cc @@ -99,14 +99,26 @@ bot->Notice(theClient, "%s has been connected for %s [since %ld]", if (Target->isModeR()) { string accountFlags; + if( Target->getAccountFlag( iClient::X_TOTP_REQ_IPR ) || Target->getAccountFlag(iClient::X_TOTP_ENABLED ) ) + { + accountFlags += ( Target->getAccountFlag( iClient::X_TOTP_REQ_IPR ) ) ? "TOTP_REQ_IPR " : "TOTP "; + + // Check for disabled methods + std::string disabled; + if( Target->getAccountFlag( iClient::X_WEB_DISABLE_TOTP ) ) + disabled += ( disabled.empty() ? "WEB" : ",WEB" ) ; + if( Target->getAccountFlag( iClient::X_CERT_DISABLE_TOTP ) ) + disabled += ( disabled.empty() ? "CERT" : ",CERT" ) ; + if( !disabled.empty() ) + accountFlags += "(DISABLE=" + disabled + ") "; + } + if(Target->getAccountFlag(iClient::X_GLOBAL_SUSPEND)) accountFlags += "SUSPENDED "; - if(Target->getAccountFlag(iClient::X_TOTP_ENABLED)) - accountFlags += "TOTP "; - if(Target->getAccountFlag(iClient::X_TOTP_REQ_IPR)) - accountFlags += "TOTP_REQ_IPR "; if(Target->getAccountFlag(iClient::X_FRAUD)) accountFlags += "FRAUD "; + if(Target->getAccountFlag(iClient::X_CERTONLY)) + accountFlags += "CERTONLY "; /* client is authed - show it here */ bot->Notice(theClient, "%s is authed as [%s]", @@ -123,6 +135,14 @@ bot->Notice( theClient, "Numeric: %s, UserModes: %s, Server Numeric: %s (%s)", targetServer->getName().c_str() ) ; +if( Target->isModeZ() ) + { + bot->Notice( theClient, "%s is connected using TLS", + st[ 1 ].c_str()) ; + if( Target->hasTlsFingerprint() ) + bot->Notice( theClient, " Fingerprint: %s", compactToCanonical( Target->getTlsFingerprint() ).c_str() ) ; + } + if( Target->isOper() ) { bot->Notice( theClient, "%s is an IRCoperator", diff --git a/mod.ccontrol/ccontrol.cc b/mod.ccontrol/ccontrol.cc index f11d025a..10132580 100644 --- a/mod.ccontrol/ccontrol.cc +++ b/mod.ccontrol/ccontrol.cc @@ -950,6 +950,9 @@ for(glineIterator GLptr = rnGlineList.begin(); GLptr != rnGlineList.end(); ++GLp rnGlineList.clear(); +// Deallocate the database handle +delete SQLDb; SQLDb = 0; + } // Register a command handler @@ -6242,6 +6245,7 @@ void ccontrol::announce(iClient* theClient, const string& text) string(), 0, 0, + string(), "Announcement Service.", ::time( 0 ) ) ; assert( newClient != 0 ); @@ -7417,6 +7421,7 @@ iClient* newClient = new (std::nothrow) iClient( string(), 0, 0, + string(), fullname, ::time( 0 ) ) ; assert( newClient != 0 ); diff --git a/mod.cloner/cloner.cc b/mod.cloner/cloner.cc index f895b972..d5f9d5de 100644 --- a/mod.cloner/cloner.cc +++ b/mod.cloner/cloner.cc @@ -868,6 +868,11 @@ if( theChan->getMode( Channel::MODE_R ) && !theClone->getMode( iClient::MODE_REGISTERED ) ) return 0 ; +/* Chanmode +Z? */ +if( theChan->getMode( Channel::MODE_Z ) + && !theClone->getMode( iClient::MODE_TLS ) ) + return 0 ; + /* Banned? */ if( banMatch( theChan, theClone ) ) return 0 ; @@ -1038,6 +1043,7 @@ iClient* newClient = new iClient( account, account_id, 0, + string(), cloneDescription, ::time( nullptr ) ) ; assert( newClient != nullptr ) ; diff --git a/mod.cservice/CERTCommand.cc b/mod.cservice/CERTCommand.cc new file mode 100644 index 00000000..654c5e1c --- /dev/null +++ b/mod.cservice/CERTCommand.cc @@ -0,0 +1,250 @@ +/** + * CERTCommand.cc + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + * + */ + +#include +#include "cservice.h" +#include "StringTokenizer.h" +#include "ELog.h" +#include "responses.h" + +namespace gnuworld +{ + +bool CERTCommand::Exec( [[maybe_unused]] iClient* theClient, [[maybe_unused]] const string& Message ) +{ +#ifdef NEW_IRCU_FEATURES + +bot->incStat("COMMANDS.CERT"); + +sqlUser* theUser = bot->isAuthed( theClient, true ) ; +if( !theUser ) + { + return false ; + } + +StringTokenizer st( Message ) ; +if( st.size() < 2 ) + { + Usage( theClient ) ; + return true ; + } + +string Command = string_upper( st[ 1 ] ) ; +if( ( Command != "ADD" ) && ( Command != "REM" ) && ( Command != "LIST" ) ) + { + Usage( theClient ) ; + return true ; + } + +if( Command == "LIST" ) + { + bot->Notice( theClient, "+-------------------------------------------------------------------------------------------------+------------------------------------------+--------------+--------------------+" ) ; + bot->Notice( theClient, "| %-95s | %-40s | %-12s | %-18s |", "TLS Fingerprint", "Added By", "Last Updated", "Note" ) ; + bot->Notice( theClient, "+-------------------------------------------------------------------------------------------------+------------------------------------------+--------------+--------------------+" ) ; + + std::stringstream theQuery ; + theQuery << "SELECT fingerprint, added_by, added_ts, note FROM users_fingerprints WHERE user_id = " + << theClient->getAccountID() + << " ORDER BY added_ts" ; + + if( !bot->SQLDb->Exec( theQuery, true ) ) + { + LOGSQL_ERROR( bot->SQLDb ) ; + return false ; + } + + if( bot->SQLDb->Tuples() > 0 ) + { + for( unsigned int i = 0 ; i < bot->SQLDb->Tuples() ; i++ ) + bot->Notice( theClient, "| %-95s | %-40s | %-12s | %-18s |", + compactToCanonical( bot->SQLDb->GetValue( i, 0 ) ).c_str(), + bot->SQLDb->GetValue( i, 1 ).c_str(), + prettyTime( std::stoul( bot->SQLDb->GetValue( i, 2 ) ), false ).c_str(), + bot->SQLDb->GetValue( i, 3 ).c_str() ) ; + bot->Notice( theClient, "+-------------------------------------------------------------------------------------------------+------------------------------------------+--------------+--------------------+" ) ; + bot->Notice( theClient, "Done listing %d fingerprint%s.", + bot->SQLDb->Tuples(), bot->SQLDb->Tuples() > 1 ? "s" : "" ) ; + } + else + { + bot->Notice( theClient, bot->getResponse( theUser, language::no_fingerprints_found ) ) ; + } + + if( theClient->hasTlsFingerprint() ) + { + bot->Notice( theClient, bot->getResponse( theUser, language::your_fingerprint_is ).c_str(), + compactToCanonical( theClient->getTlsFingerprint() ).c_str() ) ; + } + + return true ; + } + +if( Command == "ADD" ) + { + /* Check whether this user already has reached the limit of fingerprints. */ + if( bot->hasFP( theUser) > bot->getConfmaxFingerprints() ) + { + bot->Notice( theClient, bot->getResponse( theUser, language::max_fingerprints ).c_str(), bot->getConfmaxFingerprints() ) ; + return true ; + } + + string fingerPrint, note ; + + /* No param provided. Use current fingerprint, if any. */ + if( st.size() < 3 ) + { + if( theClient->hasTlsFingerprint() ) + fingerPrint = theClient->getTlsFingerprint() ; + else + { + Usage( theClient ) ; + return true ; + } + } + /* We have one param. */ + else if( st.size() == 3 ) + { + /* Is the param a fingerprint? */ + if( isValidSHA256Fingerprint( st[ 2 ] ) ) + fingerPrint = canonicalToCompact( st[ 2 ] ) ; + /* If it is not a fingerprint, we treat it as a note if (and only if) the user has a fingerprint. */ + else if( theClient->hasTlsFingerprint() ) + note = st[ 2 ] ; + /* If the param is not a fingerprint and the user does not have a fingerprint, fail. */ + else + { + bot->Notice( theClient, bot->getResponse( theUser, language::invalid_fingerprint ).c_str() ) ; + return true ; + } + } + /* We have either two params, or a note consisting of several words. */ + else if( st.size() > 3 ) + { + /* Is the first word a fingerprint? */ + if( isValidSHA256Fingerprint( st[ 2 ] ) ) + { + fingerPrint = canonicalToCompact( st[ 2 ] ) ; + note = st.assemble( 3 ) ; + } + /* If it is not a fingerprint, we treat it as a note if (and only if) the user has a fingerprint. */ + else if( theClient->hasTlsFingerprint() ) + note = st.assemble( 2 ) ; + /* If the first word is not a fingerprint and the user does not have a fingerprint, fail. */ + else + { + bot->Notice( theClient, bot->getResponse( theUser, language::invalid_fingerprint ).c_str() ) ; + return true ; + } + } + + /* Add to cache. This will return false if the fingerprint already exists. */ + auto result = bot->addFP( canonicalToCompact( fingerPrint ), theClient->getAccountID() ) ; + if( !result.second ) + { + bot->Notice( theClient, bot->getResponse( theUser, language::fingerprint_already_exists ).c_str() ) ; + return true ; + } + + /* Add to SQL. */ + std::stringstream theQuery ; + theQuery << "INSERT INTO users_fingerprints (user_id, fingerprint, added_ts, added_by, note) VALUES (" + << theClient->getAccountID() << ", '" + << fingerPrint + << "', date_part('epoch', CURRENT_TIMESTAMP)::int, '" + << theClient->getRealNickUserHost() << "', '" + << note << "')" + << std::endl ; + + if( !bot->SQLDb->Exec( theQuery, true ) ) + { + LOGSQL_ERROR( bot->SQLDb ) ; + return false ; + } + + bot->Notice( theClient, bot->getResponse( theUser, language::fingerprint_added ).c_str(), + compactToCanonical( fingerPrint ).c_str() ) ; + + return true ; + } + +if( Command == "REM" ) + { + string fingerPrint ; + + /* No fingerprint been provided. Use current, if any. */ + if( st.size() < 3 ) + { + if( theClient->hasTlsFingerprint() ) + fingerPrint = theClient->getTlsFingerprint() ; + else + { + Usage( theClient ) ; + return true ; + } + } + else + { + if( !isValidSHA256Fingerprint( st[ 2 ] ) ) + { + bot->Notice( theClient, bot->getResponse( theUser, language::invalid_fingerprint ).c_str() ) ; + return true ; + } + + fingerPrint = canonicalToCompact( st[ 2 ] ) ; + } + + /* Don't remove last fingerprint if CERTONLY is ON. */ + if( bot->hasFP( theUser) == 1 && theUser->getFlag( sqlUser::F_CERTONLY ) ) + { + bot->Notice( theClient, bot->getResponse( theUser, language::fingerprint_norem_certonly ).c_str() ) ; + return true ; + } + + if( !bot->checkFP( fingerPrint, theClient->getAccountID() ) ) + { + bot->Notice( theClient, bot->getResponse( theUser, language::fingerprint_not_found ).c_str() ) ; + return true ; + } + + /* Remove from SQL. */ + std::stringstream theQuery ; + theQuery << "DELETE FROM users_fingerprints WHERE fingerprint ='" + << fingerPrint << "'" + << std::endl ; + + if( !bot->SQLDb->Exec( theQuery, true ) ) + { + LOGSQL_ERROR( bot->SQLDb ) ; + return false ; + } + /* Remove from cache. */ + bot->removeFP( fingerPrint ) ; + + bot->Notice( theClient, bot->getResponse( theUser, language::fingerprint_removed ).c_str(), + compactToCanonical( fingerPrint ).c_str() ) ; + + return true ; + } + +#endif // NEW_IRCU_FEATURES +return true ; +} + +} // namespace gnuworld. diff --git a/mod.cservice/CHANINFOCommand.cc b/mod.cservice/CHANINFOCommand.cc index 2b4f06b7..26e09ab0 100644 --- a/mod.cservice/CHANINFOCommand.cc +++ b/mod.cservice/CHANINFOCommand.cc @@ -141,7 +141,7 @@ if( string::npos == st[ 1 ].find_first_of( '#' ) ) /* build up a flag string */ string flagsSet; - if (theUser->getFlag(sqlUser::F_GLOBAL_SUSPEND)) + if (theUser->getFlag(sqlUser::F_GLOBAL_SUSPEND)) flagsSet += "SUSPEND "; if (theUser->getFlag(sqlUser::F_INVIS)) flagsSet += "INVISIBLE "; @@ -169,10 +169,20 @@ if( string::npos == st[ 1 ].find_first_of( '#' ) ) if (adminAccess || (tmpUser == theUser)) { - if (theUser->getFlag(sqlUser::F_TOTP_REQ_IPR)) - flagsSet += "TOTP_REQ_IPR "; - else if (theUser->getFlag(sqlUser::F_TOTP_ENABLED)) - flagsSet += "TOTP "; + if (theUser->getFlag(sqlUser::F_TOTP_REQ_IPR) || theUser->getFlag(sqlUser::F_TOTP_ENABLED)) + { + flagsSet += (theUser->getFlag(sqlUser::F_TOTP_REQ_IPR)) ? "TOTP_REQ_IPR " : "TOTP "; + + // Check for disabled methods + std::string disabled; + if (theUser->getFlag(sqlUser::F_WEB_DISABLE_TOTP)) + disabled += (disabled.empty() ? "WEB" : ",WEB"); + if (theUser->getFlag(sqlUser::F_CERT_DISABLE_TOTP)) + disabled += (disabled.empty() ? "CERT" : ",CERT"); + + if (!disabled.empty()) + flagsSet += "(DISABLE=" + disabled + ") "; + } } /* flags with variables */ if (langString.size() > 0) @@ -180,13 +190,16 @@ if( string::npos == st[ 1 ].find_first_of( '#' ) ) /* flags with variables for admins (or self-viewing) only */ if (adminAccess || (tmpUser == theUser)) { + if (theUser->getFlag(sqlUser::F_AUTOHIDE)) + flagsSet += "AUTOHIDE "; + int maxLogins = theUser->getMaxLogins(); stringstream ss; ss << maxLogins; // << ends ; if (maxLogins > 1) flagsSet += "MAXLOGINS=" + ss.str() + " "; - + stringstream autoInviteQuery; autoInviteQuery << "SELECT channel_id from levels" @@ -404,10 +417,10 @@ if( string::npos == st[ 1 ].find_first_of( '#' ) ) bot->Notice(theClient, "Last Hostmask: %s", theUser->getLastHostMask().c_str()); //Show ip only to admins - if(adminAccess > 0) + if(adminAccess > 0) { bot->Notice(theClient, "Last IP: %s", - theUser->getLastIP().c_str()); + theUser->getLastIP().c_str()); } } @@ -694,7 +707,7 @@ if( !theChan ) if (bot->SQLDb->Tuples() > 0) comCount = atoi(bot->SQLDb->GetValue(0,0)); } - + // output additional information if user is admin, supporter, or applicant) if (showsupplist) { diff --git a/mod.cservice/CLEARMODECommand.cc b/mod.cservice/CLEARMODECommand.cc index 4eae2167..647a3f42 100644 --- a/mod.cservice/CLEARMODECommand.cc +++ b/mod.cservice/CLEARMODECommand.cc @@ -100,7 +100,7 @@ if(!tmpChan) return false; } -bot->ClearMode( tmpChan, string( "mstnipklrDcC" ), false ) ; +bot->ClearMode( tmpChan, string( "mstnipklrDcCuMZ" ), false ) ; bot->Notice(theClient, bot->getResponse(theUser, diff --git a/mod.cservice/HELLOCommand.cc b/mod.cservice/HELLOCommand.cc index 9393544b..583a25d9 100644 --- a/mod.cservice/HELLOCommand.cc +++ b/mod.cservice/HELLOCommand.cc @@ -180,12 +180,24 @@ string cryptpass = bot->CryptPass(plainpass); string updatedBy = "HELLO used by: "; updatedBy += theClient->getNickUserHost().c_str(); -newUser = new (std::nothrow) sqlUser(bot->SQLDb); +newUser = new (std::nothrow) sqlUser(bot); newUser->setUserName(escapeSQLChars(st[1].c_str())); newUser->setEmail(escapeSQLChars(st[2])); newUser->setPassword(cryptpass.c_str()); newUser->setLastUpdatedBy(updatedBy); newUser->setFlag(sqlUser::F_INVIS); + +string err ; +auto recOpt = make_scram_sha256_record( plainpass, &err ) ; +if( !recOpt ) + { + LOG( ERROR, "[SCRAM] Record generation error: {}", err ) ; + } +else + { + std::string scram_record = *recOpt ; + newUser->setScramRecord( scram_record ) ; + } newUser->Insert(); bot->Notice(theClient, "I generated this password for you: \002%s\002", diff --git a/mod.cservice/LOGINCommand.cc b/mod.cservice/LOGINCommand.cc index e397893d..640bfb68 100644 --- a/mod.cservice/LOGINCommand.cc +++ b/mod.cservice/LOGINCommand.cc @@ -37,16 +37,11 @@ namespace gnuworld { -using std::string ; -using std::endl ; -using std::ends ; -using std::stringstream ; -using namespace gnuworld; bool LOGINCommand::Exec( iClient* theClient, const string& Message ) { StringTokenizer st( Message ) ; -if( st.size() < 3 ) +if( st.size() < 2 ) { Usage(theClient); return true; @@ -65,181 +60,25 @@ if (tmpUser) tmpUser->getUserName().c_str()); return false; } -unsigned int maxFailedLogins = bot->getConfigVar("MAX_FAILED_LOGINS")->asInt(); -unsigned int failedLogins = bot->getFailedLogins(theClient); -if ((maxFailedLogins > 0) && (failedLogins >= maxFailedLogins)) -{ - /* exceeded maximum failed logins */ - bot->Notice(theClient, - bot->getResponse(tmpUser, - language::max_failed_logins, - string("AUTHENTICATION FAILED as %s (Exceeded maximum login failures for this session)")).c_str(), - st[1].c_str()); - return false; -} - -sqlUser* theUser; -int auth_res = bot->authenticateUser(st[1],st.assemble(2),theClient,&theUser); -unsigned int loginTime = bot->getUplink()->getStartTime() + bot->getConfloginDelay(); -unsigned int max_failed_logins = bot->getConfigVar("FAILED_LOGINS")->asInt(); -unsigned int failed_login_rate = bot->getConfigVar("FAILED_LOGINS_RATE")->asInt(); -string clientList; - -switch(auth_res) - { - case cservice::TOO_EARLY_TOLOGIN: - bot->Notice(theClient, "AUTHENTICATION FAILED as %s (Unable " - "to login during reconnection, please try again in " - "%i seconds)", - st[1].c_str(), (loginTime - bot->currentTime())); - return false; - break; - case cservice::AUTH_FAILED: - bot->setFailedLogins(theClient, failedLogins+1); - bot->Notice(theClient, "AUTHENTICATION FAILED as %s", st[1].c_str()); - return false; - break; - case cservice::AUTH_UNKNOWN_USER: - bot->setFailedLogins(theClient, failedLogins+1); - bot->Notice(theClient, - bot->getResponse(tmpUser, - language::not_registered, - string("AUTHENTICATION FAILED as %s")).c_str(), - st[1].c_str()); - return false; - break; - case cservice::AUTH_SUSPENDED_USER: - bot->setFailedLogins(theClient, failedLogins+1); - bot->Notice(theClient, "AUTHENTICATION FAILED as %s (Suspended)", - st[1].c_str()); - return false; - break; - case cservice::AUTH_NO_TOKEN: - bot->setFailedLogins(theClient, failedLogins+1); - bot->Notice(theClient,"AUTHENTICATION FAILED as %s (Missing TOTP token)",st[1].c_str()); - return false; - break; - case cservice::AUTH_INVALID_PASS: - if (failed_login_rate==0) - failed_login_rate = 900; - bot->setFailedLogins(theClient, failedLogins+1); - bot->Notice(theClient, - bot->getResponse(theUser, - language::auth_failed, - string("AUTHENTICATION FAILED as %s")).c_str(), - theUser->getUserName().c_str()); - /* increment failed logins counter */ - theUser->incFailedLogins(); - if ((max_failed_logins > 0) && (theUser->getFailedLogins() > max_failed_logins) && - (theUser->getLastFailedLoginTS() < (time(NULL) - failed_login_rate))) - { - /* we have exceeded our maximum - alert relay channel */ - /* work out a checksum for the password. Yes, I could have - * just used a checksum of the original password, but this - * means it's harder to 'fool' the check digit with a real - * password - create MD5 from original salt stored */ - unsigned char checksum; - md5 hash; - md5Digest digest; - - if (theUser->getPassword().size() < 9) - { - checksum = 0; - } else { - string salt = theUser->getPassword().substr(0, 8); - string guess = salt + st.assemble(2); - - hash.update( (const unsigned char *)guess.c_str(), guess.size() ); - hash.report( digest ); - - checksum = 0; - for (size_t i = 0; i < MD5_DIGEST_LENGTH; i++) - { - /* add ascii value to check digit */ - checksum += digest[i]; - } - } - - theUser->setLastFailedLoginTS(time(NULL)); - bot->logPrivAdminMessage("%d failed logins for %s (last attempt by %s, checksum %d).", - theUser->getFailedLogins(), - theUser->getUserName().c_str(), - theClient->getRealNickUserHost().c_str(), - checksum); - } - return false; - break; - case cservice::AUTH_ERROR: - bot->Notice(theClient,"AUTHENTICATION FAILED as %s due to an error, please contact CService represetitive",st[1].c_str()); - return false; - break; - case cservice::AUTH_INVALID_TOKEN: - bot->setFailedLogins(theClient, failedLogins+1); - bot->Notice(theClient, - bot->getResponse(theUser, - language::auth_failed_token, - string("AUTHENTICATION FAILED as %s (Invalid Token)")).c_str(), - theUser->getUserName().c_str()); - /* increment failed logins counter */ - theUser->incFailedLogins(); - return false; - break; - case cservice::AUTH_FAILED_IPR: - bot->setFailedLogins(theClient, failedLogins+1); - bot->Notice(theClient, "AUTHENTICATION FAILED as %s (IPR)", - st[1].c_str()); - /* notify the relay channel */ - bot->logAdminMessage("%s (%s) failed IPR check.", - theClient->getNickName().c_str(), - st[1].c_str()); - /* increment failed logins counter */ - theUser->incFailedLogins(); - if ((max_failed_logins > 0) && (theUser->getFailedLogins() > max_failed_logins) && - (theUser->getLastFailedLoginTS() < (time(NULL) - failed_login_rate))) - { - /* we have exceeded our maximum - alert relay channel */ - theUser->setLastFailedLoginTS(time(NULL)); - bot->logPrivAdminMessage("%d failed logins for %s (last attempt by %s).", - theUser->getFailedLogins(), - theUser->getUserName().c_str(), - theClient->getRealNickUserHost().c_str()); - } - return false; - break; - case cservice::AUTH_ML_EXCEEDED: - bot->setFailedLogins(theClient, failedLogins+1); - bot->Notice(theClient, "AUTHENTICATION FAILED as %s (Maximum " - "concurrent logins exceeded).", - theUser->getUserName().c_str()); - - for( sqlUser::networkClientListType::iterator ptr = theUser->networkClientList.begin() ; - ptr != theUser->networkClientList.end() ; ) - { - clientList += (*ptr)->getNickUserHost(); - ++ptr; - if (ptr != theUser->networkClientList.end()) - { - clientList += ", "; - } - } // for() - - bot->Notice(theClient, "Current Sessions: %s", clientList.c_str()); - return false; - break; - case cservice::AUTH_SUCCEEDED: - break; - default: - //Should never get here! - LOG_MSG( ERROR, "Response {auth_res} while authenticating!") - .with( "client", theClient ) - .logStructured() ; - bot->Notice(theClient,"AUTHENTICATION FAILED as %s (due to an error)\n",st[1].c_str()); - return false; - break; - } - -return bot->doCommonAuth(theClient, theUser->getUserName()); +cservice::AuthStruct auth = { + AuthType::LOGIN, // auth type + cservice::AUTH_ERROR, // result (placeholder) + st[ 1 ], // username + st.assemble( 2 ), // password/token + theClient->getUserName(), // ident + xIP( theClient->getIP() ).GetNumericIP(), // ip + theClient->getTlsFingerprint(), // tls fingerprint + nullptr, // sqlUser (placeholder) + theClient, // iClient + {} // no sasl +} ; + +cservice::AuthResult auth_res = bot->authenticateUser( auth ) ; +auth.result = auth_res ; +bot->processAuthentication( auth ) ; + +return true; } } // namespace gnuworld. diff --git a/mod.cservice/Makefile.am b/mod.cservice/Makefile.am index 54782798..587e0fdc 100755 --- a/mod.cservice/Makefile.am +++ b/mod.cservice/Makefile.am @@ -11,6 +11,7 @@ libcservice_la_CXXFLAGS = -I$(top_srcdir)/db -I${top_srcdir}/include \ libcservice_la_SOURCES = \ mod.cservice/banMatcher.cc \ mod.cservice/cservice.cc \ + mod.cservice/cservice_crypt.cc \ mod.cservice/networkData.cc \ mod.cservice/sqlChannel.cc \ mod.cservice/sqlUser.cc \ @@ -24,6 +25,7 @@ libcservice_la_SOURCES = \ mod.cservice/ADDUSERCommand.cc \ mod.cservice/BANCommand.cc \ mod.cservice/BANLISTCommand.cc \ + mod.cservice/CERTCommand.cc \ mod.cservice/CHANINFOCommand.cc \ mod.cservice/CLEARMODECommand.cc \ mod.cservice/DEOPCommand.cc \ @@ -82,6 +84,8 @@ EXTRA_DIST += \ mod.cservice/constants.h \ mod.cservice/cserviceCommands.h \ mod.cservice/cservice_config.h \ + mod.cservice/cservice_confvars.h \ + mod.cservice/cservice_crypt.h \ mod.cservice/cservice.h \ mod.cservice/levels.h \ mod.cservice/networkData.h \ diff --git a/mod.cservice/NEWPASSCommand.cc b/mod.cservice/NEWPASSCommand.cc index df1cb38f..c27bcbef 100644 --- a/mod.cservice/NEWPASSCommand.cc +++ b/mod.cservice/NEWPASSCommand.cc @@ -139,6 +139,19 @@ output << ends; // Prepend the md5 hash to the salt string finalPassword = salt + output.str().c_str(); tmpUser->setPassword(finalPassword); +std::string err ; +// elog << "Generating SCRAM record for user " << theUser->getUserName() << " and password: [" << st.assemble( 0, pass_end ) << "]\n" ; +auto recOpt = make_scram_sha256_record( st.assemble( 1 ), &err ) ; + +if( !recOpt ) + { + LOG( ERROR, "SCRAM generation error: {}", err ) ; + } +else + { + std::string scram_record = *recOpt ; + tmpUser->setScramRecord( scram_record ) ; + } if( tmpUser->commit(theClient) ) { diff --git a/mod.cservice/SETCommand.cc b/mod.cservice/SETCommand.cc index 7f4bdd6e..06aa777d 100644 --- a/mod.cservice/SETCommand.cc +++ b/mod.cservice/SETCommand.cc @@ -147,6 +147,79 @@ if( st[1][0] != '#' ) // Didn't find a hash? return true; } +#ifdef NEW_IRCU_FEATURES + if (option == "AUTOHIDE") + { + if (value == "ON") + { + theUser->setFlag(sqlUser::F_AUTOHIDE); + theUser->commit(theClient); + bot->Notice(theClient, + bot->getResponse(theUser, + language::autohide_on, + string("Your AUTOHIDE setting is now ON."))); + return true; + } + + if (value == "OFF") + { + theUser->removeFlag(sqlUser::F_AUTOHIDE); + theUser->commit(theClient); + bot->Notice(theClient, + bot->getResponse(theUser, + language::autohide_off, + string("Your AUTOHIDE setting is now OFF."))); + return true; + } + + bot->Notice(theClient, + bot->getResponse(theUser, + language::set_cmd_syntax_on_off, + string("value of %s must be ON or OFF")).c_str(), + option.c_str()); + return true; + } + + if (option == "CERTONLY") + { + if (value == "ON") + { + if (!bot->hasFP(theUser)) + { + bot->Notice(theClient, + bot->getResponse(theUser, + language::no_fingerprints_registered, + string("You currently don't have any fingerprints added to your account. For more information, use '/msg X help cert'"))); + return true; + } + theUser->setFlag(sqlUser::F_CERTONLY); + theUser->commit(theClient); + bot->Notice(theClient, + bot->getResponse(theUser, + language::certonly_on, + string("Your CERTONLY setting is now ON."))); + return true; + } + + if (value == "OFF") + { + theUser->removeFlag(sqlUser::F_CERTONLY); + theUser->commit(theClient); + bot->Notice(theClient, + bot->getResponse(theUser, + language::certonly_off, + string("Your CERTONLY setting is now OFF."))); + return true; + } + + bot->Notice(theClient, + bot->getResponse(theUser, + language::set_cmd_syntax_on_off, + string("value of %s must be ON or OFF")).c_str(), + option.c_str()); + return true; + } +#endif #ifdef USE_NOTES if (option == "NONOTES") { diff --git a/mod.cservice/constants.h b/mod.cservice/constants.h index d63e2da3..623fc9b1 100644 --- a/mod.cservice/constants.h +++ b/mod.cservice/constants.h @@ -38,7 +38,7 @@ namespace sql * articles of data. */ const std::string channel_fields = "id,name,flags,mass_deop_pro,flood_pro,url,channels.description,comment,keywords,registered_ts,channel_ts,channel_mode,userflags,channels.last_updated,limit_offset,limit_period,limit_grace,limit_max,max_bans,no_take,welcome,limit_joinmax,limit_joinsecs,limit_joinperiod,limit_joinmode"; - const std::string user_fields = "users.id,users.user_name,users.password,users.url,users.language_id,users.flags,users.last_updated_by,users.last_updated,users.signup_ts,users.email,users.maxlogins,users.verificationdata,users.totp_key"; + const std::string user_fields = "users.id,users.user_name,users.password,users.url,users.language_id,users.flags,users.last_updated_by,users.last_updated,users.signup_ts,users.email,users.maxlogins,users.verificationdata,users.totp_key,users.scram_record"; const std::string level_fields = "channel_id,user_id,access,flags,suspend_expires,suspend_level,suspend_by,added,added_by,last_Modif,last_Modif_By,last_Updated,suspend_reason"; const std::string ban_fields = "id,channel_id,banmask,set_by,set_ts,level,expires,reason,last_updated"; } diff --git a/mod.cservice/cservice.cc b/mod.cservice/cservice.cc index 14bb738d..cf35fd55 100644 --- a/mod.cservice/cservice.cc +++ b/mod.cservice/cservice.cc @@ -179,7 +179,7 @@ MyUplink->RegisterEvent( EVT_XQUERY, this ); MyUplink->RegisterEvent( EVT_XREPLY, this ); MyUplink->RegisterEvent( EVT_GLINE , this ); MyUplink->RegisterEvent( EVT_REMGLINE , this ); - +MyUplink->RegisterEvent( EVT_NETBREAK, this ); xClient::OnAttach() ; } @@ -218,7 +218,9 @@ RegisterCommand(new SHOWIGNORECommand(this, "SHOWIGNORE", "", 3)); RegisterCommand(new SUPPORTCommand(this, "SUPPORT", "#channel ", 15)); RegisterCommand(new NOTECommand(this, "NOTE", "send , read all, erase ", 10)); RegisterCommand(new NOTECommand(this, "NOTES", "send , read all, erase ", 10)); - +#ifdef NEW_IRCU_FEATURES +RegisterCommand(new CERTCommand(this, "CERT", " [fingerprint] [note]", 10)); +#endif RegisterCommand(new OPCommand(this, "OP", "<#channel> [nick] [nick] ..", 3)); RegisterCommand(new DEOPCommand(this, "DEOP", "<#channel> [nick] [nick] ..", 3)); RegisterCommand(new VOICECommand(this, "VOICE", "<#channel> [nick] [nick] ..", 3)); @@ -404,6 +406,9 @@ preloadLevelsCache(); /* Preload any user accounts we want to */ preloadUserCache(); +/* Preload fingerprints */ +preloadFingerprintCache(); + /* Load the glines from db */ loadGlines(); @@ -434,6 +439,8 @@ for( commandMapType::iterator ptr = commandMap.begin() ; } commandMap.clear() ; +saslRequests.clear() ; +fingerprintMap.clear() ; } void cservice::BurstChannels() @@ -516,7 +523,35 @@ void cservice::BurstChannels() void cservice::OnConnect() { -// TODO: I changed this from return 0 +#ifdef NEW_IRCU_FEATURES +auto saslServer = Network->findNetConf( "sasl.server" ) ; +if( !saslServer || saslServer->first != MyUplink->getName() ) + { + MyUplink->Write( "%s CF %d sasl.server :%s", + getCharYY().c_str(), + time(nullptr), + MyUplink->getName().c_str() ) ; + } + +auto saslMechanisms = Network->findNetConf( "sasl.mechanisms" ) ; +if( !saslMechanisms || saslMechanisms->first != saslMechsAdvertiseList() ) + { + MyUplink->Write( "%s CF %d sasl.mechanisms :%s", + getCharYY().c_str(), + time(nullptr), + saslMechsAdvertiseList().c_str() ) ; + } + +auto netSaslTimeout = Network->findNetConf( "sasl.timeout" ) ; +if( !netSaslTimeout || std::stoul( netSaslTimeout->first ) != saslTimeout ) + { + MyUplink->Write( "%s CF %d sasl.timeout :%d", + getCharYY().c_str(), + time(nullptr), + saslTimeout ) ; + } +#endif // NEW_IRCU_FEATURES + xClient::OnConnect() ; } @@ -1931,6 +1966,30 @@ bool cservice::hasIPR( sqlUser* theUser ) return true; } +unsigned int cservice::hasFP( sqlUser* theUser ) +{ +stringstream theQuery ; +theQuery << "SELECT COUNT(*) FROM " + << "users_fingerprints WHERE user_id = " + << theUser->getID() + << endl ; +#ifdef LOG_SQL +elog << "cservice::hasFP::sqlQuery> " + << theQuery.str() + << endl ; +#endif + +if( !SQLDb->Exec(theQuery, true ) ) + { + elog << "cservice::hasFP> SQL Error: " + << SQLDb->ErrorMessage() + << endl; + return 0 ; + } + +return SQLDb->Tuples() ; +} + /** * Check a user against IP restrictions */ @@ -3000,6 +3059,7 @@ void cservice::processDBUpdates() updateUsers(); updateLevels(); updateBans(); + updateFingerprints(); LOG( INFO, "[DB-UPDATE]: Complete."); } @@ -3310,6 +3370,39 @@ void cservice::updateUsers() lastUserRefresh = atoi(SQLDb->GetValue(0,"db_unixtime").c_str()); } +/* + * Check the fingerpritns table to see if there have been any updates since we last looked. + */ +void cservice::updateFingerprints() +{ +const std::string theQuery = "SELECT fingerprint,user_id FROM users_fingerprints" ; + +#ifdef LOG_SQL +elog << "*** [CMaster::updateFingerprints]: sqlQuery: " + << theQuery + << endl ; +#endif + +if( !SQLDb->Exec(theQuery, true ) ) + { + elog << "*** [CMaster::updateFingerprints]: SQL error: " + << SQLDb->ErrorMessage() + << endl ; + return; + } + +if( SQLDb->Tuples() <= 0 ) + { + return ; + } + +fingerprintMap.clear() ; +for( unsigned int i = 0 ; i < SQLDb->Tuples() ; i++ ) + fingerprintMap.emplace( SQLDb->GetValue( i, 0 ), std::stoul( SQLDb->GetValue( i, 1 ) ) ) ; + +LOG(INFO, "[DB-UPDATE]: Refreshed fingerprint(s)." ) ; +} + void cservice::updateBans() { /* Todo */ @@ -4911,12 +5004,25 @@ void cservice::OnEvent( const eventType& theEvent, { switch( theEvent ) { + case EVT_NETBREAK: + { + iServer* theServer = static_cast< iServer* >( data1 ) ; + saslRequests.erase( + std::remove_if( + saslRequests.begin(), + saslRequests.end(), + [&]( const SaslRequest& req ) { return req.theServer == theServer ; } + ), + saslRequests.end() + ) ; + break ; + } case EVT_XQUERY: { iServer* theServer = static_cast< iServer* >( data1 ); const char* Routing = reinterpret_cast< char* >( data2 ); const char* Message = reinterpret_cast< char* >( data3 ); - //elog << "CSERVICE.CC XQUERY: " << theServer->getName() << " " << Routing << " " << Message << endl; + // elog << "CSERVICE.CC XQUERY: " << theServer->getName() << " " << Routing << " " << Message << endl; //As it is possible to run multiple GNUWorld clients on one server, first parameter should be a nickname. //If it ain't us, ignore the message, the message is probably meant for another client here. StringTokenizer st( Message ) ; @@ -4935,6 +5041,10 @@ switch( theEvent ) { doXQIsCheck(theServer, Routing, Command, Message); } + if (Command == "SASL") + { + doXQSASL(theServer, Routing, Message); + } break; } case EVT_XREPLY: @@ -7599,6 +7709,29 @@ void cservice::preloadUserCache() << endl; } +void cservice::preloadFingerprintCache() +{ + stringstream theQuery; + theQuery << "SELECT fingerprint,user_id FROM users_fingerprints" + << endl; + + elog << "*** [CMaster::preloadFingerprintCache]: Loading TLS fingerprints : " + << endl; + + if( SQLDb->Exec(theQuery, true ) ) + { + for (unsigned int i = 0 ; i < SQLDb->Tuples(); i++) + { + fingerprintMap.emplace(SQLDb->GetValue(i,0), std::stoul(SQLDb->GetValue(i,1))); + } + } + + elog << "*** [CMaster::preloadFingerprintCache]: Done. Loaded " + << SQLDb->Tuples() + << " fingerprints." + << endl; +} + void cservice::incStat(const string& name) { statsMapType::iterator ptr = statsMap.find( name ); @@ -7881,6 +8014,26 @@ void cservice::doCoderStats(iClient* theClient) } userDBTOTPIPRTotal = atoi(SQLDb->GetValue(0,0)); + int userDBFPTotal = 0; + theQuery.str(""); + theQuery << "SELECT COUNT(*) FROM users,users_fingerprints WHERE " + << "users.id = users_fingerprints.user_id" + << endl; +#ifdef LOG_SQL + elog << "cservice::doCoderStats::sqlQuery> " + << theQuery.str().c_str() + << endl; +#endif + + if( !SQLDb->Exec(theQuery, true ) ) + { + elog << "cservice::doCoderStats> SQL Error: " + << SQLDb->ErrorMessage() + << endl; + return; + } + userDBFPTotal = atoi(SQLDb->GetValue(0,0)); + float userTotal = userCacheHits + userHits; float userEf = userCacheHits ? ((float)userCacheHits / userTotal * 100) : 0; @@ -7953,6 +8106,7 @@ void cservice::doCoderStats(iClient* theClient) Notice(theClient, "--- Total users in DB with IPR: %i (in cache: %i (%.2f%% of total))", userDBIPRTotal, iprCount, cacheIPRTotal); float cacheIPRTOTPTotal = ((float)ipr_totp_Count / (float)sqlUserCache.size()) * 100; Notice(theClient, "--- Total users in DB with TOTP&IPR: %i (in cache: %i (%.2f%% of total))", userDBTOTPIPRTotal, ipr_totp_Count, cacheIPRTOTPTotal); + Notice(theClient, "--- Total users in DB with TLS: %i (fingerprints in cache %i)", userDBFPTotal, fingerprintMap.size()); float authTotal = ((float)authCount / (float)Network->clientList_size()) * 100; Notice(theClient, "--- Total Auth'd : %i (%.2f%% of total)", @@ -8216,143 +8370,915 @@ else } } -int cservice::authenticateUser(const string& username, const string& password, const string& ip, const string& ident,unsigned int& ipr_ts,sqlUser** suser) +cservice::AuthResult cservice::authenticateUser( AuthStruct& auth ) { +unsigned int ipr_ts ; +bool certAuth = false ; // Set to true if the client has a certificate matching the username + +/* 1: Check loginDelay. */ unsigned int useLoginDelay = getConfigVar("USE_LOGIN_DELAY")->asInt(); unsigned int loginTime = getUplink()->getStartTime() + loginDelay; if ( (useLoginDelay == 1) && (loginTime >= (unsigned int)currentTime()) ) - { - return TOO_EARLY_TOLOGIN; - } -/* - * Find the user record, confirm authorisation and attach the record - * to this client. - */ -if(username[0] == '#') - { - return AUTH_FAILED; - } + { + return TOO_EARLY_TOLOGIN ; + } -// TODO: Force a refresh of the user's info from the db -*suser = getUserRecord(username); +/* 2: Check whether this iClient has exceeded the maximum number of failed login attempts. */ +if( auth.theClient ) + { + unsigned int maxFailedLogins = getConfigVar("MAX_FAILED_LOGINS")->asInt() ; + unsigned int failedLogins = getFailedLogins( auth.theClient ) ; + if( ( maxFailedLogins > 0 ) && ( failedLogins >= maxFailedLogins ) ) + return AUTH_FAIL_EXCEEDED ; + } -sqlUser* theUser = *suser; +/* 3: Check that the username exists. */ +// TODO: Force a refresh of the user's info from the db +sqlUser* theUser = getUserRecord( auth.username ) ; if( !theUser ) - { - return AUTH_UNKNOWN_USER; - } + { + return AUTH_UNKNOWN_USER ; + } -if (theUser->getFlag(sqlUser::F_GLOBAL_SUSPEND)) - { - return AUTH_SUSPENDED_USER; - } -StringTokenizer st (password); -int pass_end = st.size(); +auth.theUser = theUser ; + +/* 4: Check whether the username is glbobally suspended. */ +if( theUser->getFlag( sqlUser::F_GLOBAL_SUSPEND ) ) + { + return AUTH_SUSPENDED_USER ; + } + +/** + * 5: If the client has a fingerprint, we check whether it matches the username. + * If it does, we take that into account when parsing the password string to look for + * the TOTP token (if any). If there is no match, we treat the password string as a + * regular login. + * + * If the client is authenticating with EXTERNAL, the auth fails if the fingerprint does not match the username. + * + */ +if( !auth.fingerprint.empty() ) + { + auto it = fingerprintMap.find( auth.fingerprint ) ; + if( it != fingerprintMap.end() && it->second == theUser->getID() ) + certAuth = true ; + } + +if( auth.sasl == SaslMechanism::EXTERNAL && !certAuth ) + { + return AUTH_INVALID_FINGERPRINT ; + } + +/* 6: If CERTONLY is set for this user, the auth fails if we did not get a fingerprint match. */ +if( theUser->getFlag( sqlUser::F_CERTONLY ) && !certAuth ) + { + return AUTH_CERTONLY ; + } + +/** + * 7: Check whether a TOTP token has been provided. + * TOTP is not required for fingerprint authentication if F_CERT_DISABLE_TOTP is set. + */ +StringTokenizer st( auth.password ) ; +StringTokenizer::size_type pass_end = st.size() ; #ifdef TOTP_AUTH_ENABLED -bool totp_enabled = false; -if(totpAuthEnabled && theUser->getFlag(sqlUser::F_TOTP_ENABLED)) { - if(st.size() == 1 ) { - return AUTH_NO_TOKEN; - } - pass_end = st.size()-1; - totp_enabled = true; -} +bool totp_enabled = false ; + +if( totpAuthEnabled && theUser->getFlag( sqlUser::F_TOTP_ENABLED ) ) + { + /* If the user has a certificate matching the username or we are using SCRAM, the password is the TOTP token. */ + if( ( certAuth && !theUser->getFlag( sqlUser::F_CERT_DISABLE_TOTP ) ) + || auth.sasl == SaslMechanism::SCRAM_SHA_256 ) + { + if( st.size() < 1 ) + return AUTH_NO_TOKEN ; + + pass_end = st.size() - 1 ; + totp_enabled = true ; + } + /* If the user does not have a certificate matching the username and we are not using SCRAM, the second part of the password is the TOTP token. */ + else if( !certAuth && auth.sasl != SaslMechanism::SCRAM_SHA_256 ) + { + if( st.size() < 2 ) + return AUTH_NO_TOKEN ; + + pass_end = st.size() - 1 ; + totp_enabled = true ; + } + } #endif -/* - * Check if this is a privileged user, if so check against IP restrictions +/** + * 8: IPR + * Check if this is a privileged user, if so check against IP restrictions. */ -if (needIPRcheck(theUser)) -{ - /* ok, they have "*" access (excluding alumni's) */ - if (!checkIPR(ip, theUser, ipr_ts)) +if( needIPRcheck( theUser ) ) { - return AUTH_FAILED_IPR; + /* ok, they have "*" access (excluding alumni's) */ + if( !checkIPR( auth.ip, theUser, ipr_ts ) ) + return AUTH_FAILED_IPR ; } -} /* - * Check password, if its wrong, bye bye. + * 9: Check password. + * Password will not be checked for EXTERNAL or SCRAM or when a matching certificate is provided. */ -if (!isPasswordRight(theUser, st.assemble(0,pass_end))) - { - return AUTH_INVALID_PASS; - } +if( !certAuth && auth.sasl != SaslMechanism::SCRAM_SHA_256 + && auth.sasl != SaslMechanism::EXTERNAL + && !isPasswordRight( theUser, st.assemble( 0, pass_end ) ) ) + { + return AUTH_INVALID_PASS ; + } + +/* + * Compute SCRAM RECORD if it does not exist. + */ +#ifdef HAVE_LIBSSL +if( !certAuth && auth.sasl != SaslMechanism::SCRAM_SHA_256 + && auth.sasl != SaslMechanism::EXTERNAL + && theUser->getScramRecord().empty() ) + { + std::string err ; + auto recOpt = make_scram_sha256_record( st.assemble( 0, pass_end ), &err ) ; + + if( !recOpt ) + { + LOG( ERROR, "[SCRAM] Record generation error: {}", err ) ; + } + else + { + std::string scram_record = *recOpt ; + theUser->setScramRecord( scram_record ) ; + if( auth.theClient ) + theUser->commit( auth.theClient ) ; + else + theUser->commit( auth.ident + "@" + auth.ip ) ; + } + } +#endif // HAVE_LIBSSL + +/* 10: Check TOTP token. */ #ifdef TOTP_AUTH_ENABLED -if(totp_enabled) { - char* key; - size_t len; - int res = oath_base32_decode(theUser->getTotpKey().c_str(),theUser->getTotpKey().size(),&key,&len); - if(res != OATH_OK) { - return AUTH_ERROR; - } - res=oath_totp_validate(key,len,time(NULL),30,0,1,st[st.size()-1].c_str()); - free(key); - if(res < 0 ) { - return AUTH_INVALID_TOKEN; +if( totp_enabled ) + { + char* key ; + size_t len ; + int res = oath_base32_decode( theUser->getTotpKey().c_str(), theUser->getTotpKey().size(), &key, &len ) ; + if( res != OATH_OK ) + return AUTH_ERROR ; + + res = oath_totp_validate( key, len, time(NULL), 30, 0, 1, st[ pass_end ].c_str() ) ; + free( key ) ; + if( res < 0 ) + return AUTH_INVALID_TOKEN; + } +#endif + +/* 12: Don't exceed MAXLOGINS. */ +bool iploginallow = false ; +string clip = fixToCIDR64( auth.ip.c_str() ) ; +if( theUser->networkClientList.size() + 1 > theUser->getMaxLogins() ) + { + /* They have exceeded their maxlogins setting, but check if they + * are allowed to login from the same IP - only applies if their + * maxlogins is set to ONE. + */ + uint32_t iplogins = getConfigVar("LOGINS_FROM_SAME_IP")->asInt() ; + uint32_t iploginident = getConfigVar("LOGINS_FROM_SAME_IP_AND_IDENT")->asInt() ; + if( ( theUser->getMaxLogins() == 1 ) && ( iplogins > 1 ) ) + { + /* ok, we're using the multi-logins feature (0=disabled) */ + if( theUser->networkClientList.size() + 1 <= iplogins ) + { + /* Check their IP from previous session against + * current IP. If it matches, allow the login. + * As this only applies if their maxlogin is 1, we + * know there is only 1 entry in their clientlist. + */ + if( clip == ( xIP( theUser->networkClientList.front()->getIP()).GetNumericIP( true ) ) ) + { + if( iploginident == 1 ) + { + /* need to check ident here */ + string oldident = theUser->networkClientList.front()->getUserName() ; + if( ( oldident[0] =='~' ) || ( oldident == auth.ident ) ) + { + /* idents match (or they are unidented) - allow this login */ + iploginallow = true ; + } + } + else + { + /* don't need to check ident, this login is allowed */ + iploginallow = true ; + } + } + } } + + if( !iploginallow ) + return AUTH_ML_EXCEEDED; + } + +/* Success. */ +if( auth.theClient ) + setIPRts( auth.theClient, ipr_ts ) ; + +return AUTH_SUCCEEDED ; } -#endif -/* - * Don't exceed MAXLOGINS. +/* This function is called after authenticateUser() to process the authentication. */ +bool cservice::processAuthentication( AuthStruct auth, std::string* Message ) +{ +/* Get background variables. */ +unsigned int loginTime = getUplink()->getStartTime() + loginDelay ; +unsigned int max_failed_logins = getConfigVar("FAILED_LOGINS")->asInt() ; +unsigned int failed_login_rate = getConfigVar("FAILED_LOGINS_RATE")->asInt() ; + +/* Fetch client info. */ +string clientMask ; +if( auth.theClient ) + clientMask = auth.theClient->getRealNickUserHost() ; +else + clientMask = auth.ident + "@" + auth.ip ; + +string authResponse ; +bool setFailedLogin = false ; + +switch( auth.result ) + { + case cservice::TOO_EARLY_TOLOGIN: + authResponse = TokenStringsParams( "AUTHENTICATION FAILED as %s (Unable to login during reconnection, please try again in %i seconds)", + auth.username.c_str(), ( loginTime - currentTime() ) ) ; + break ; + case cservice::AUTH_FAILED: + authResponse = TokenStringsParams( getResponse( NULL, language::auth_failed, + string("AUTHENTICATION FAILED as %s") ).c_str(), + auth.username.c_str() ) ; + setFailedLogin = true ; + break ; + case cservice::AUTH_UNKNOWN_USER: + authResponse = TokenStringsParams( getResponse( NULL, language::not_registered, + string("AUTHENTICATION FAILED as %s") ).c_str(), + auth.username.c_str() ) ; + setFailedLogin = true ; + break ; + case cservice::AUTH_SUSPENDED_USER: + authResponse = TokenStringsParams( "AUTHENTICATION FAILED as %s (Suspended)", + auth.username.c_str() ) ; + setFailedLogin = true ; + break ; + case cservice::AUTH_NO_TOKEN: + authResponse = TokenStringsParams( "AUTHENTICATION FAILED as %s (Missing TOTP token)", + auth.username.c_str() ) ; + setFailedLogin = true ; + break ; + case cservice::AUTH_INVALID_PASS: + if( failed_login_rate == 0 ) + failed_login_rate = 900 ; + authResponse = TokenStringsParams( getResponse( NULL, language::auth_failed, + string("AUTHENTICATION FAILED as %s") ).c_str(), + auth.username.c_str() ) ; + + if( ( max_failed_logins > 0 ) && ( auth.theUser->getFailedLogins() > max_failed_logins ) && + ( auth.theUser->getLastFailedLoginTS() < ( time(NULL) - failed_login_rate ) ) ) + { + /* we have exceeded our maximum - alert relay channel */ + /* work out a checksum for the password. Yes, I could have + * just used a checksum of the original password, but this + * means it's harder to 'fool' the check digit with a real + * password - create MD5 from original salt stored */ + unsigned char checksum ; + md5 hash ; + md5Digest digest ; + + if( auth.theUser->getPassword().size() < 9 ) + checksum = 0 ; + else + { + string salt = auth.theUser->getPassword().substr( 0, 8 ) ; + string guess = salt + auth.password ; + + hash.update( (const unsigned char *)guess.c_str(), guess.size() ) ; + hash.report( digest ) ; + + checksum = 0 ; + for( size_t i = 0; i < MD5_DIGEST_LENGTH; i++ ) + { + /* add ascii value to check digit */ + checksum += digest[ i ] ; + } + } + + auth.theUser->setLastFailedLoginTS( time(NULL) ) ; + logPrivAdminMessage("%d failed logins for %s (last attempt by %s%s, checksum %d).", + auth.theUser->getFailedLogins(), + auth.theUser->getUserName().c_str(), + clientMask.c_str(), + auth.type == AuthType::XQUERY ? " (LoC)" : "", + checksum ) ; + } + setFailedLogin = true ; + break ; + case cservice::AUTH_INVALID_FINGERPRINT: + authResponse = TokenStringsParams( "AUTHENTICATION FAILED as %s (Invalid fingerprint)", + auth.username.c_str() ) ; + setFailedLogin = true ; + break ; + case cservice::AUTH_ERROR: + authResponse = TokenStringsParams( "AUTHENTICATION FAILED as %s due to an error. Please contact a CService representative", + auth.username.c_str() ) ; + break ; + case cservice::AUTH_INVALID_TOKEN: + authResponse = TokenStringsParams( getResponse( NULL, language::auth_failed_token, + string("AUTHENTICATION FAILED as %s") ).c_str(), + auth.username.c_str() ) ; + + setFailedLogin = true ; + break ; + case cservice::AUTH_FAILED_IPR: + authResponse = TokenStringsParams("AUTHENTICATION FAILED as %s (IPR)", + auth.username.c_str() ) ; + + /* notify the relay channel */ + logAdminMessage( "%s (%s) failed IPR check.", + clientMask.c_str(), + auth.username.c_str() ) ; + + if( ( max_failed_logins > 0 ) && ( auth.theUser->getFailedLogins() > max_failed_logins ) && + ( auth.theUser->getLastFailedLoginTS() < ( time(NULL) - failed_login_rate ) ) ) + { + /* we have exceeded our maximum - alert relay channel */ + auth.theUser->setLastFailedLoginTS( time(NULL) ) ; + logPrivAdminMessage( "%d failed logins for %s (last attempt by %s).", + auth.theUser->getFailedLogins(), + auth.theUser->getUserName().c_str(), + clientMask.c_str() ) ; + } + setFailedLogin = true ; + break ; + case cservice::AUTH_CERTONLY: + authResponse = TokenStringsParams( "AUTHENTICATION FAILED as %s (Password login disabled and fingerprint mismatch)", + auth.username.c_str() ) ; + setFailedLogin = true ; + break ; + case cservice::AUTH_ML_EXCEEDED: + authResponse = TokenStringsParams( "AUTHENTICATION FAILED as %s (Maximum concurrent logins exceeded)", + auth.theUser->getUserName().c_str() ) ; + + /* Do not list the sessions for XQUERY logins. */ + if( auth.theClient ) + { + string clientList ; + for( sqlUser::networkClientListType::iterator ptr = auth.theUser->networkClientList.begin() ; + ptr != auth.theUser->networkClientList.end() ; ) + { + clientList += (*ptr)->getNickUserHost() ; + ++ptr ; + if( ptr != auth.theUser->networkClientList.end() ) + clientList += ", " ; + } // for() + + Notice( auth.theClient, "Current Sessions: %s", clientList.c_str() ) ; + } + setFailedLogin = true ; + break ; + case cservice::AUTH_FAIL_EXCEEDED: + authResponse = TokenStringsParams( getResponse( NULL, language::max_failed_logins, + string("AUTHENTICATION FAILED as %s (Exceeded maximum login failures for this session)") ).c_str(), + auth.username.c_str() ) ; + setFailedLogin = true ; + break ; + case cservice::AUTH_SUCCEEDED: + break ; + default: + //Should never get here! + elog << "Response " << auth.result << " while authenticating!" << endl ; + authResponse = TokenStringsParams( "AUTHENTICATION FAILED as %s (due to an error)", + auth.username.c_str() ) ; + break ; + } + +if( setFailedLogin ) + { + if( auth.theClient ) + { + unsigned int failedLogins = getFailedLogins( auth.theClient ) ; + setFailedLogins( auth.theClient, failedLogins + 1 ) ; + } + + if( auth.theUser ) + auth.theUser->incFailedLogins() ; + } + +/* Send response only if it fails. If it succeeds, the message is sent from doCommonAuth() */ +if( auth.theClient ) + { + if( auth.result == AUTH_SUCCEEDED ) + { + doCommonAuth( auth.theClient, auth.theUser->getUserName() ) ; + return true ; + } + else + { + Notice( auth.theClient, authResponse ) ; + return false ; + } + } +/** + * If theClient is nullptr, it means that the authentication is done by SASL/XQUERY and auth will + * occurr when the client is introduced to the network. */ +else + { + *Message = authResponse ; + } -bool iploginallow = false; -//unsigned long clip = xIP(ip.c_str(),false).GetLongIP(); -//string clip = xIP(xIP(ip.c_str(),false).GetLongIP()).GetNumericIP(); -string clip = fixToCIDR64(ip.c_str()); -if(theUser->networkClientList.size() + 1 > theUser->getMaxLogins()) { - /* They have exceeded their maxlogins setting, but check if they - are allowed to login from the same IP - only applies if their - maxlogins is set to ONE */ - uint32_t iplogins = getConfigVar("LOGINS_FROM_SAME_IP")->asInt(); - uint32_t iploginident = getConfigVar("LOGINS_FROM_SAME_IP_AND_IDENT")->asInt(); - if ((theUser->getMaxLogins() == 1) && (iplogins > 1)) - { - /* ok, we're using the multi-logins feature (0=disabled) */ - if (theUser->networkClientList.size() + 1 <= iplogins) - { - /* Check their IP from previous session against - current IP. If it matches, allow the login. - As this only applies if their maxlogin is 1, we - know there is only 1 entry in their clientlist */ - if (clip == (xIP(theUser->networkClientList.front()->getIP()).GetNumericIP(true))) //->getIP() - { - if (iploginident==1) - { - /* need to check ident here */ - string oldident = theUser->networkClientList.front()->getUserName(); - if ((oldident[0]=='~') || (oldident==ident)) - { - /* idents match (or they are unidented) - allow this login */ - iploginallow = true; - } - } else { - /* don't need to check ident, this login is allowed */ - iploginallow = true; - } - } - } - } - if (!iploginallow) - { - return AUTH_ML_EXCEEDED; - } +return ( auth.result == AUTH_SUCCEEDED ) ; } -return AUTH_SUCCEEDED; + +bool cservice::parseSaslMechanism( const std::string& in, cservice::SaslMechanism& out ) +{ + std::string up = string_upper( in ) ; +#define X(NAME, NAME_STR) if( up == NAME_STR ) { out = cservice::SaslMechanism::NAME ; return true ; } + CSERVICE_SASL_MECH_LIST +#undef X + return false ; } -int cservice::authenticateUser(const string& username, const string& password, iClient* theClient,sqlUser** theUser) { - unsigned int ipr_ts; - const string ip = xIP(theClient->getIP()).GetNumericIP(); - int res = authenticateUser(username,password, ip, theClient->getUserName(),ipr_ts,theUser); - if(res == AUTH_SUCCEEDED) - { - setIPRts(theClient, ipr_ts); +std::string cservice::saslMechanismToString( cservice::SaslMechanism mech ) +{ +#define X(NAME, NAME_STR) if( mech == cservice::SaslMechanism::NAME ) return NAME_STR ; + CSERVICE_SASL_MECH_LIST +#undef X + return "" ; +} + +std::string cservice::saslMechsAdvertiseList() +{ +// std::string out = "MECHS " ; + string out ; + bool first = true ; +#define X(NAME, NAME_STR) \ + do { if( !first ) out += "," ; first = false ; out += string_lower( NAME_STR ) ; } while(0); + CSERVICE_SASL_MECH_LIST +#undef X + return out ; +} + +bool cservice::doXQSASL( iServer* theServer, const string& Routing, const string& Message ) +{ + elog << "cservice::doXQSASL: Routing: " << Routing << " Message: " << Message << "\n" ; + +#ifdef HAVE_LIBSSL + StringTokenizer st( Message ) ; + if( st.size() < 2 ) + { + elog << "Received empty SASL message... Ignoring." << endl; + return false ; + } + + // Delete timed out requests. + auto it = saslRequests.begin() ; + while( it != saslRequests.end() ) + { + if( currentTime() - it->last_ts > saslTimeout ) + it = saslRequests.erase(it) ; + else + ++it ; + } + + // Check if we have an existing entry with this routing and server + it = std::find_if( + saslRequests.begin(), + saslRequests.end(), + [&]( const SaslRequest& req ) + { return req.routing == Routing && req.theServer == theServer ; } + ) ; + + /* Found existing challenge. */ + if( it != saslRequests.end() ) + { + elog << "Found matching SASL request for Routing: " << Routing << "\n" ; + string authMessage = st[ 1 ] ; + + it->last_ts = currentTime() ; + + // If the message is 400 bytes long we add it to the struct and wait for the next message. + if( authMessage.size() == 400 ) + { + it->credentials += authMessage ; + return true ; + } + + if( authMessage != "+" ) + it->credentials += authMessage ; + + if( it->mechanism == SaslMechanism::PLAIN ) + { + auto bufferOpt = b64decode( it->credentials, nullptr, true ) ; + if( !bufferOpt ) + { + elog << "[PLAIN] Failed to decode credentials: " << it->credentials << endl ; + incStat("SASL." + saslMechanismToString( it->mechanism ) + ".ERROR" ) ; + doXResponse( theServer, Routing, "Invalid credentials", true ) ; + saslRequests.erase( it ) ; + return false ; + } + + string decodedString = std::string( reinterpret_cast< char* >( bufferOpt->data() ), bufferOpt->size() ) ; + size_t p1 = decodedString.find( '\0' ) ; + size_t p2 = decodedString.find( '\0', p1 + 1 ) ; + + if( p1 == std::string::npos || p2 == std::string::npos || p1 == 0 ) + { + elog << "[PLAIN] Invalid PLAIN format in decoded credentials: " << decodedString << endl ; + incStat("SASL." + saslMechanismToString( it->mechanism ) + ".ERROR" ) ; + doXResponse( theServer, Routing, "Invalid credentials", true ) ; + saslRequests.erase( it ) ; + return false ; + } + + it->username = decodedString.substr( p1 + 1, p2 - ( p1 + 1 ) ) ; + it->password = decodedString.substr( p2 + 1 ) ; + + if( it->username.empty() || it->password.empty() ) + { + elog << "[PLAIN] Empty username or password in credentials" << endl ; + incStat("SASL." + saslMechanismToString( it->mechanism ) + ".ERROR" ) ; + doXResponse( theServer, Routing, "Invalid credentials", true ) ; + saslRequests.erase( it ) ; + return false ; + } + } + else if( it->mechanism == SaslMechanism::EXTERNAL ) + { + // A client certificate has to be provided. + if( it->fingerprint.empty() ) + { + incStat("SASL." + saslMechanismToString( it->mechanism ) + ".FAILED" ) ; + doXResponse( theServer, Routing, "Client certificate required for SASL EXTERNAL authentication", true ) ; + saslRequests.erase( it ) ; + return false ; + } + + // We only support EXTERNAL if a username has been provided. + if( it->credentials.empty() ) + { + incStat("SASL." + saslMechanismToString( it->mechanism ) + ".FAILED" ) ; + doXResponse( theServer, Routing, "Username required for SASL EXTERNAL authentication", true ) ; + saslRequests.erase( it ) ; + return false ; + } + + auto bufferOpt = b64decode( it->credentials, nullptr, true ) ; + if( !bufferOpt ) + { + elog << "[EXTERNAL] Failed to decode username: " << it->credentials << endl ; + incStat("SASL." + saslMechanismToString( it->mechanism ) + ".ERROR" ) ; + doXResponse( theServer, Routing, "Invalid username", true ) ; + saslRequests.erase( it ) ; + return false ; + } + + string decodedString = std::string( reinterpret_cast< char* >( bufferOpt->data() ), bufferOpt->size() ) ; + StringTokenizer st2( decodedString ) ; + + // Validate that we have at least a username + if( st2.size() < 1 || st2[ 0 ].empty() ) + { + elog << "[EXTERNAL] Invalid username in decoded credentials" << endl ; + incStat("SASL." + saslMechanismToString( it->mechanism ) + ".ERROR" ) ; + doXResponse( theServer, Routing, "Invalid username", true ) ; + saslRequests.erase( it ) ; + return false ; + } + + it->username = st2[ 0 ] ; + if( st2.size() > 1 ) + it->password = st2[ 1 ] ; + } + else if( it->mechanism == SaslMechanism::SCRAM_SHA_256 ) + { + if( it->state == SaslState::INITIAL ) + { + auto bufferOpt = b64decode( it->credentials, nullptr, true ) ; + if( !bufferOpt ) + { + elog << "[SCRAM] Failed to decode client first message: " << it->credentials << endl ; + incStat("SASL." + saslMechanismToString( it->mechanism ) + ".ERROR" ) ; + doXResponse( theServer, Routing, "Invalid credentials", true ) ; + saslRequests.erase( it ) ; + return false ; + } + + string decodedString = std::string( reinterpret_cast< char* >( bufferOpt->data() ), bufferOpt->size() ) ; + if( decodedString.find("n,,") != 0 ) + { + elog << "[SCRAM] Invalid client first message: " << decodedString << endl ; + incStat("SASL." + saslMechanismToString( it->mechanism ) + ".ERROR" ) ; + doXResponse( theServer, Routing, "Invalid credentials", true ) ; + saslRequests.erase( it ) ; + return false ; + } + + it->client_first = decodedString.substr( 3 ) ; // remove 'n,,' + StringTokenizer st2( it->client_first, ',' ) ; + for( size_t i = 0 ; i < st2.size() ; ++i ) + { + if( st2[ i ].rfind( "n=", 0 ) == 0 ) + { + StringTokenizer st3( st2[ i ].substr( 2 ) ) ; + it->username = st3[ 0 ] ; + if( st3.size() > 1 ) + it->password = st3[ 1 ] ; + } + else if( st2[ i ].rfind( "r=", 0 ) == 0 ) + it->client_nonce = st2[ i ].substr( 2 ) ; + } + + if( it->username.empty() || it->client_nonce.empty() ) + { + elog << "[SCRAM] Missing username or client nonce in authentication: " << decodedString << endl ; + incStat("SASL." + saslMechanismToString( it->mechanism ) + ".ERROR" ) ; + doXResponse( theServer, Routing, "Invalid credentials", true ) ; + saslRequests.erase( it ) ; + return false ; + } + + sqlUser* theUser = getUserRecord( it->username ) ; + if( !theUser || theUser->getScramRecord().empty() ) + { + incStat("SASL." + saslMechanismToString( it->mechanism ) + ".FAILED" ) ; + doXResponse( theServer, Routing, "AUTHENTICATION FAILED as " + it->username, true ) ; + saslRequests.erase( it ) ; + return false ; + } + + it->username = theUser->getUserName() ; + + auto nonceOpt = generateRandomNonce() ; + if( !nonceOpt ) + { + elog << "[SCRAM] Failed to generate server nonce." << endl ; + incStat("SASL." + saslMechanismToString( it->mechanism ) + ".ERROR" ) ; + doXResponse( theServer, Routing, "Internal error", true ) ; + saslRequests.erase( it ) ; + return false ; + } + it->server_nonce = *nonceOpt ; + std::string combinedNonce = it->client_nonce + it->server_nonce ; + + std::string err ; + auto parse_opt = parse_scram_sha256_record( theUser->getScramRecord(), &err ) ; + if( !parse_opt ) + { + incStat("SASL." + saslMechanismToString( it->mechanism ) + ".ERROR" ) ; + LOG( ERROR, "[SCRAM] Record parse error: {}", err ) ; + doXResponse( theServer, Routing, "Internal error", true ) ; + return false ; + } + it->scram = *parse_opt ; + + /*elog << "Iterations: " << it->scram.iterations << endl; + elog << "Salt (base64): " << b64encode(it->scram.salt.data(), it->scram.salt.size()) << endl; + elog << "StoredKey (base64): " << b64encode(it->scram.storedKey.data(), it->scram.storedKey.size()) << endl; + elog << "ServerKey (base64): " << b64encode(it->scram.serverKey.data(), it->scram.serverKey.size()) << endl; + elog << "StoredKey (hex): "; + for (auto c : it->scram.storedKey) elog << std::hex << (int)c << " "; + elog << std::dec << "\n"; + elog << "ServerKey (hex): "; + for (auto c : it->scram.serverKey) elog << std::hex << (int)c << " "; + elog << std::dec << "\n";*/ + + it->server_first = "r=" + combinedNonce + ",s=" + b64encode( it->scram.salt.data(), it->scram.salt.size() ) + ",i=" + std::to_string( it->scram.iterations ) ; + std::string serverFirst_b64 = b64encode( reinterpret_cast< const unsigned char* >( it->server_first.data() ), it->server_first.size() ) ; + MyUplink->XReply( theServer, Routing, "SASL " + serverFirst_b64 ) ; + it->state = SaslState::SERVER_FIRST ; + it->credentials.clear() ; + return true ; + } + else if( it->state == SaslState::SERVER_FIRST ) + { + auto bufferOpt = b64decode( it->credentials, nullptr, true ) ; + if( !bufferOpt ) + { + elog << "[SCRAM] Failed to decode client final message: " << it->credentials << endl ; + incStat("SASL." + saslMechanismToString( it->mechanism ) + ".ERROR" ) ; + doXResponse( theServer, Routing, "Invalid credentials", true ) ; + saslRequests.erase( it ) ; + return false ; + } + + string decodedString = std::string( reinterpret_cast< char* >( bufferOpt->data() ), bufferOpt->size() ) ; + if( decodedString.find("n,,") == 0 ) + decodedString = decodedString.substr( 3 ) ; + + size_t p_pos = decodedString.rfind( ",p=" ) ; + if( p_pos == std::string::npos ) + { + elog << "[SCRAM] Missing ,p= in client final message: " << decodedString << endl ; + incStat("SASL." + saslMechanismToString( it->mechanism ) + ".ERROR" ) ; + doXResponse( theServer, Routing, "Invalid credentials", true ) ; + saslRequests.erase( it ) ; + return false ; + } + it->client_final = decodedString.substr(0, p_pos) ; // up to but not including ",p=" + + std::string cbind_input, nonce, proof ; + StringTokenizer st( decodedString, ',' ) ; + for( size_t i = 0 ; i < st.size() ; ++i ) + { + if( st[i].rfind("c=", 0 ) == 0 ) + cbind_input = st[i].substr( 2 ) ; + else if( st[i].rfind("r=", 0 ) == 0 ) + nonce = st[i].substr( 2 ) ; + else if( st[i].rfind("p=", 0 ) == 0 ) + proof = st[i].substr( 2 ) ; + } + + if( cbind_input.empty() || nonce.empty() || proof.empty() ) + { + elog << "[SCRAM] Missing c=, r= or p= in client final message: " << decodedString << endl ; + incStat("SASL." + saslMechanismToString( it->mechanism ) + ".ERROR" ) ; + doXResponse( theServer, Routing, "Invalid credentials", true ) ; + saslRequests.erase( it ) ; + return false ; + } + + if( nonce != it->client_nonce + it->server_nonce ) + { + elog << "[SCRAM] nonce match failed: " << decodedString << endl ; + incStat("SASL." + saslMechanismToString( it->mechanism ) + ".FAILED" ) ; + doXResponse( theServer, Routing, "AUTHENTICATION FAILED as " + it->username, true ) ; + saslRequests.erase( it ) ; + return false ; + } + + std::string auth_message = it->client_first + "," + it->server_first + "," + it->client_final ; + bool valid = validate_scram_sha256_proof( + it->scram.storedKey, + auth_message, + proof + ) ; + + if( !valid ) + { + elog << "[SCRAM] Proof validation failed for user " << it->username << endl ; + incStat("SASL." + saslMechanismToString( it->mechanism ) + ".FAILED" ) ; + doXResponse( theServer, Routing, "AUTHENTICATION FAILED as " + it->username, true ) ; + saslRequests.erase( it ) ; + return false ; + } + + std::string server_signature = compute_server_signature( it->scram.serverKey, auth_message ) ; + std::string server_final = "v=" + server_signature ; + std::string server_final_b64 = b64encode( reinterpret_cast< const unsigned char* >( server_final.data() ), server_final.size() ) ; + MyUplink->XReply( theServer, Routing, "SASL " + server_final_b64 ) ; + it->state = SaslState::COMPLETE ; + return true ; // Waiting for the + to complete the authentication + } + else if( it->state == SaslState::COMPLETE ) + { + /* Pass through to authentication. */ + } + else + { + elog << "[SCRAM] Invalid state in authentication." << endl ; + incStat("SASL." + saslMechanismToString( it->mechanism ) + ".ERROR" ) ; + doXResponse( theServer, Routing, "An error occurred", true ) ; + saslRequests.erase( it ) ; + return false ; + } + } + + AuthStruct auth = { + AuthType::XQUERY, + AUTH_ERROR, // result (placeholder) + it->username, // username + it->password, // password/token + it->ident, // ident (empty for LoC) + it->ip, // ip + it->fingerprint, // tls fingerprint + nullptr, // sqlUser (placeholder) + it->theClient, // iClient (nullptr for LoC) + it->mechanism + } ; + + if( it->mechanism == SaslMechanism::SCRAM_SHA_256 ) + elog << "Authenticating with SCRAM" << endl; + else + elog << "Authenticating with username " << it->username << " and password " << mask(it->password) << endl ; + + AuthResult auth_res = authenticateUser( auth ) ; + auth.result = auth_res ; + + /* Process result. */ + if( auth_res == AUTH_SUCCEEDED ) + { + incStat("SASL." + saslMechanismToString( it->mechanism ) + ".SUCCESS" ) ; + if( !auth.theClient ) + doXResponse( theServer, Routing, auth.theUser->getUserName() + ":" + + std::to_string( auth.theUser->getID() ) + ":" + + std::to_string( makeAccountFlags( auth.theUser ) ) + + (auth.theUser->getFlag( sqlUser::F_AUTOHIDE ) ? " +x" : "") ) ; + else + { + doCommonAuth( auth.theClient, auth.theUser->getUserName() ) ; + doXResponse( theServer, Routing, string() ) ; + } + + elog << "cservice::doXQSASL: " + << "Succesful auth for " + << it->username + << endl ; + + saslRequests.erase( it ) ; + return true; + } + + /* The authentication failed. Process and send correct message. */ + string AuthResponse ; + processAuthentication( auth, &AuthResponse ) ; + + /* Send response. */ + doXResponse( theServer, Routing, AuthResponse, true ) ; + + incStat("SASL." + saslMechanismToString( it->mechanism ) + ".FAILED" ) ; + + saslRequests.erase( it ) ; + return false ; + } + // It is the initial message consisting of the mechanism. + else + { + // Did we receive a numeric? + iClient* tmpClient = Network->findClient( st[ 1 ] ) ; + string IP ; + string fingerprint ; + string authMessage ; + if( tmpClient ) + { + if( st.size() < 3 ) + { + elog << "Bogus SASL message with numeric" << endl ; + doXResponse( theServer, Routing, "An error has occurred", true ) ; + return false ; + } + + IP = xIP( tmpClient->getIP() ).GetNumericIP() ; + fingerprint = tmpClient->getTlsFingerprint().empty() ? "_" : tmpClient->getTlsFingerprint() ; + authMessage = string_upper( st[ 2 ] ) ; + } + else + { + if( st.size() < 4 ) + { + elog << "Bogus SASL message without numeric" << endl ; + doXResponse( theServer, Routing, "An error has occurred", true ) ; + return false ; + } + + IP = st[ 1 ] ; + fingerprint = st[ 2 ] ; + authMessage = string_upper( st[ 3 ] ) ; + } + + SaslMechanism mech ; + if( !parseSaslMechanism( authMessage, mech ) ) + { + LOG( ERROR, "Received invalid SASL mechanism: {}", authMessage ) ; + doXResponse( theServer, Routing, "An error has occurred", true ) ; + return false ; + } + + if( mech == SaslMechanism::EXTERNAL && fingerprint == "_" ) + { + incStat("SASL." + saslMechanismToString( mech ) + ".FAILED" ) ; + doXResponse( theServer, Routing, "No TLS fingerprint provided", true ) ; + return false ; + } + + // Valid mechanism - store in cache for further use. + elog << "Received valid SASL mechanism: " << authMessage << " from IP: " << IP << endl; + SaslRequest newRequest ; + newRequest.theClient = tmpClient ; + newRequest.routing = Routing ; + newRequest.theServer = theServer ; + newRequest.ip = IP ; + newRequest.added_ts = currentTime() ; + newRequest.last_ts = currentTime() ; + newRequest.fingerprint = fingerprint == "_" ? "" : fingerprint ; + newRequest.mechanism = mech ; + newRequest.ident = tmpClient ? tmpClient->getUserName() : "" ; + + saslRequests.push_back( newRequest ) ; + + MyUplink->XReply( theServer, Routing, "SASL +" ) ; } - return res; +#endif // HAVE_LIBSSL + return true; } bool cservice::doXQLogin(iServer* theServer, const string& Routing, const string& Message) @@ -8364,12 +9290,9 @@ bool cservice::doXQLogin(iServer* theServer, const string& Routing, const string StringTokenizer st( Message ); string username; string password; - string ip = string(); - string hostname = string(); - string ident = string(); - sqlUser* theUser; - unsigned int ipr_ts; - string AuthResponse = string(); + string ip; + string hostname; + string ident; if (st[0] == "LOGIN") { @@ -8402,155 +9325,47 @@ bool cservice::doXQLogin(iServer* theServer, const string& Routing, const string ident = st[3]; LOG( TRACE, "XQ-LOGIN2: LOGIN2 {} {} {} {} {}", ip, hostname, ident, username, mask(password) ) ; } - int auth_res = authenticateUser(username,password,ip,ident,ipr_ts,&theUser); - unsigned int loginTime = getUplink()->getStartTime() + loginDelay; - unsigned int max_failed_logins = getConfigVar("FAILED_LOGINS")->asInt(); - unsigned int failed_login_rate = getConfigVar("FAILED_LOGINS_RATE")->asInt(); - switch(auth_res) - { - case TOO_EARLY_TOLOGIN: - AuthResponse = TokenStringsParams("AUTHENTICATION FAILED as %s (Unable " - "to login during reconnection, please try again in " - "%i seconds)", - username.c_str(), (loginTime - currentTime())); - doXResponse(theServer, Routing, AuthResponse,true); - LOG( DEBUG, "Auth res = TOO_EARLY_TOLOGIN" ) ; - break; - case AUTH_FAILED: - AuthResponse = TokenStringsParams("AUTHENTICATION FAILED as %s (Erroneus username)", username.c_str()); - doXResponse(theServer, Routing, AuthResponse, true); - LOG( DEBUG, "Auth res = AUTH_FAILED" ) ; - break; - case AUTH_UNKNOWN_USER: - AuthResponse = TokenStringsParams("AUTHENTICATION FAILED as %s", username.c_str()); - doXResponse(theServer, Routing, AuthResponse, true); - LOG( DEBUG, "Auth res = AUTH_UNKNOWN_USER" ) ; - break; - case AUTH_SUSPENDED_USER: - AuthResponse = TokenStringsParams("AUTHENTICATION FAILED as %s (Suspended)", theUser->getUserName().c_str()); - doXResponse(theServer, Routing, AuthResponse, true); - LOG( DEBUG, "Auth res = AUTH_SUSPENDED_USER" ) ; - break; - case AUTH_NO_TOKEN: - theUser->incFailedLogins(); - AuthResponse = TokenStringsParams("AUTHENTICATION FAILED as %s (Missing TOTP token)", theUser->getUserName().c_str()); - doXResponse(theServer, Routing, AuthResponse, true); - LOG( DEBUG, "Auth res = AUTH_NO_TOKEN" ) ; - break; - case AUTH_INVALID_PASS: - if (failed_login_rate==0) - failed_login_rate = 900; - AuthResponse = TokenStringsParams(getResponse(theUser, - language::auth_failed, - string("AUTHENTICATION FAILED as %s")).c_str(), - theUser->getUserName().c_str()); - doXResponse(theServer, Routing, AuthResponse, true); // <- this will be removed! - LOG( DEBUG, "Auth res = AUTH_INVALID_PASS" ) ; - /* increment failed logins counter */ - theUser->incFailedLogins(); - if ((max_failed_logins > 0) && (theUser->getFailedLogins() > max_failed_logins) && - (theUser->getLastFailedLoginTS() < (time(NULL) - failed_login_rate))) - { - /* we have exceeded our maximum - alert relay channel */ - /* work out a checksum for the password. Yes, I could have - * just used a checksum of the original password, but this - * means it's harder to 'fool' the check digit with a real - * password - create MD5 from original salt stored */ - unsigned char checksum; - md5 hash; - md5Digest digest; - - if (theUser->getPassword().size() < 9) - { - checksum = 0; - } else { - string salt = theUser->getPassword().substr(0, 8); - string guess = salt + st.assemble(2); - - hash.update( (const unsigned char *)guess.c_str(), guess.size() ); - hash.report( digest ); - - checksum = 0; - for (size_t i = 0; i < MD5_DIGEST_LENGTH; i++) - { - /* add ascii value to check digit */ - checksum += digest[i]; - } - } - theUser->setLastFailedLoginTS(time(NULL)); - logPrivAdminMessage("%d failed logins for %s (last attempt by %s@%s (LoC), checksum %d).", - theUser->getFailedLogins(), - theUser->getUserName().c_str(), - ident.c_str(), - ip.c_str(), - checksum); - } - break; - case AUTH_ERROR: - AuthResponse = TokenStringsParams("AUTHENTICATION FAILED as %s due to an error, please contact CService represetitive", username.c_str()); - doXResponse(theServer, Routing, AuthResponse, true); - LOG( DEBUG, "Auth res = AUTH_ERROR" ) ; - break; - case AUTH_INVALID_TOKEN: - theUser->incFailedLogins(); - AuthResponse = TokenStringsParams(getResponse(theUser, - language::auth_failed_token, - string("AUTHENTICATION FAILED as %s (Invalid Token)")).c_str(), - theUser->getUserName().c_str()); - doXResponse(theServer, Routing, AuthResponse, true); - LOG( DEBUG, "Auth res = AUTH_INVALID_TOKEN" ) ; - break; - case AUTH_FAILED_IPR: - /* increment failed logins counter */ - theUser->incFailedLogins(); - AuthResponse = TokenStringsParams("AUTHENTICATION FAILED as %s (IPR)", theUser->getUserName().c_str()); - /* notify the relay channel */ - logAdminMessage("%s@%s (%s) failed IPR check.", - ident.c_str(), - ip.c_str(), - theUser->getUserName().c_str()); - if ((max_failed_logins > 0) && (theUser->getFailedLogins() > max_failed_logins) && - (theUser->getLastFailedLoginTS() < (time(NULL) - failed_login_rate))) - { - /* we have exceeded our maximum - alert relay channel */ - theUser->setLastFailedLoginTS(time(NULL)); - logPrivAdminMessage("%d failed logins for %s (last attempt by %s@%s).", - theUser->getFailedLogins(), - theUser->getUserName().c_str(), - ident.c_str(), ip.c_str()); - } - doXResponse(theServer, Routing, AuthResponse, true); - LOG( DEBUG, "Auth res = AUTH_FAILED_IPR" ) ; - break; - case AUTH_ML_EXCEEDED: - /* increment failed logins counter */ - theUser->incFailedLogins(); - AuthResponse = TokenStringsParams("AUTHENTICATION FAILED as %s (Maximum " - "concurrent logins exceeded).", - theUser->getUserName().c_str()); - doXResponse(theServer, Routing, AuthResponse, true); - LOG( DEBUG, "Auth res = AUTH_ML_EXCEEDED" ) ; - break; - case AUTH_SUCCEEDED: - doXResponse(theServer, Routing, theUser->getUserName() + ":" + std::to_string(theUser->getID()) + ":" + std::to_string(makeAccountFlags(theUser))); - LOG_MSG( DEBUG, "Succesful auth for {user_name}" ) - .with( "user", theUser ) - .logStructured() ; - incStat("CORE.LOC.SUCCESS"); - return true; - break; - default: - //Should never get here! - LOG( ERROR, "Response {} while authenticating!", auth_res ) ; - AuthResponse = TokenStringsParams("AUTHENTICATION FAILED as %s (due to an error)\n", username.c_str()); - doXResponse(theServer, Routing, AuthResponse, true); - break; + AuthStruct auth = { + AuthType::XQUERY, // auth type + AUTH_ERROR, // result (placeholder) + username, // username + password, // password/token + ident, // ident + ip, // ip + string(), // tls fingerprint + nullptr, // sqlUser (placeholder) + nullptr, // iClient (not in use for LoC) + SaslMechanism::NO_SASL + } ; + + AuthResult auth_res = authenticateUser( auth ) ; + auth.result = auth_res ; + + /* Process result. */ + if( auth_res == AUTH_SUCCEEDED ) + { + incStat( "LOC." + st[0] + ".SUCCESS" ) ; + doXResponse(theServer, Routing, auth.theUser->getUserName() + ":" + + std::to_string(auth.theUser->getID()) + ":" + + std::to_string(makeAccountFlags(auth.theUser)) + + (auth.theUser->getFlag( sqlUser::F_AUTOHIDE ) ? " +x" : "") ) ; + elog << "cservice::doXQLogin: " + << "Succesful auth for " + << username + << endl; + return true; } -LOG( DEBUG, "XQ-LOGIN: FAILED login for {}", username ) ; -incStat("CORE.LOC.FAILED"); -return true; + /* The authentication failed. Process and send correct message. */ + string AuthResponse; + processAuthentication(auth, &AuthResponse); + + /* Send response. */ + doXResponse(theServer, Routing, AuthResponse, true); + incStat( "LOC." + st[0] + ".FAILED" ) ; + + return true; } void cservice::doXQToAllServices(const string& Routing, const string& Message) @@ -9128,6 +9943,11 @@ bool cservice::doCommonAuth(iClient* theClient, string username) if (!LoC) this->MyUplink->UserLogin(theClient, theUser->getUserName(), theUser->getID(), makeAccountFlags(theUser), this); +#ifdef NEW_IRCU_FEATURES + /* Set remote +x if user has AUTOHIDE set */ + if (theUser->getFlag(sqlUser::F_AUTOHIDE) && !theClient->isModeX()) + MyUplink->Write("%s OM %s :+x", getCharYY().c_str(), theClient->getCharYYXXX().c_str()); +#endif /* * If the user account has been suspended, make sure they don't get * auto-opped. @@ -9141,7 +9961,7 @@ bool cservice::doCommonAuth(iClient* theClient, string username) return true; } - + /* * Check they aren't banned < 75 in any chan. */ @@ -9796,7 +10616,7 @@ const iClient::flagType newFlags = makeAccountFlags( theUser ) ; if( theClient->getAccountFlags() == newFlags ) return ; -#ifdef USE_AC_XFLAGS +#ifdef NEW_IRCU_FEATURES MyUplink->Write( "%s AC %s %s %u %u", getCharYY().c_str(), theClient->getCharYYXXX().c_str(), theClient->getAccount().c_str(), theClient->getAccountID(), newFlags ) ; diff --git a/mod.cservice/cservice.h b/mod.cservice/cservice.h index 5f2e508a..12c1c29a 100644 --- a/mod.cservice/cservice.h +++ b/mod.cservice/cservice.h @@ -41,6 +41,7 @@ #include "EConfig.h" #include "cservice_config.h" #include "cservice_confvars.h" +#include "cservice_crypt.h" #include "cserviceCommands.h" #include "sqlChannel.h" #include "sqlUser.h" @@ -53,7 +54,7 @@ #include "prometheus.h" #ifdef USE_THREAD -#include "threadworker.h" + #include "threadworker.h" #endif namespace gnuworld @@ -62,6 +63,11 @@ using std::string ; using std::vector ; using std::map ; +enum class AuthType { + LOGIN, + XQUERY +} ; + /* * Class for the configuration variables extracted from the database. * An attempted conversion to a number is stored in 'int_value' for @@ -90,6 +96,62 @@ class Command; class cservice : public xClient { +private: + /* + * Declare SASL mechanisms once using an X-macro list. Each entry: + * X(EnumName, CanonicalName) + * CanonicalName may be any consistent casing; helpers will convert as needed. + */ +#define CSERVICE_SASL_MECH_LIST \ + X( PLAIN, "PLAIN" ) \ + X( SCRAM_SHA_256, "SCRAM-SHA-256" ) \ + X( EXTERNAL, "EXTERNAL" ) + + enum class SaslMechanism { + NO_SASL, +#define X(NAME, NAME_STR) NAME, + CSERVICE_SASL_MECH_LIST +#undef X + } ; + + enum class SaslState { + INITIAL, // Waiting for the first client message + CLIENT_FIRST, // Received client-first-message (SCRAM step 1) + SERVER_FIRST, // Sent server-first-message, waiting for client-final-message (SCRAM step 2) + CLIENT_FINAL, // Received client-final-message (SCRAM step 3) + COMPLETE, // Authentication complete (success or failure) + FAILED // Authentication failed + } ; + + struct SaslRequest { + iServer* theServer ; + iClient* theClient = nullptr ; + time_t added_ts ; + time_t last_ts ; + string routing ; + string ip ; + string ident ; + string fingerprint ; + string username ; + string password ; + SaslMechanism mechanism ; + string credentials ; + string client_nonce ; + string server_nonce ; + string client_first ; + string server_first ; + string client_final ; + SaslState state = SaslState::INITIAL ; + #ifdef HAVE_LIBSSL + ScramParsed scram ; + #endif + } ; + + std::vector< SaslRequest > saslRequests ; + static bool parseSaslMechanism( const std::string&, SaslMechanism& ) ; + static std::string saslMechanismToString( cservice::SaslMechanism ) ; + static std::string saslMechsAdvertiseList() ; + protected: /* Configfile */ @@ -187,6 +249,9 @@ class cservice : public xClient // Accesslevel cache, key is pair(userid, chanid). typedef map < std::pair , sqlLevel* > sqlLevelHashType ; + // Fingerprints. Key is fingerprint, value is userID. + typedef map< string, unsigned int > fpMapType ; + // Cache of user records. sqlUserHashType sqlUserCache; @@ -199,6 +264,9 @@ class cservice : public xClient // Cache of Level records. sqlLevelHashType sqlLevelCache; + // Cache of TLS fingerprints. + fpMapType fingerprintMap; + /* Pushover object. */ std::shared_ptr< PushoverClient > pushover ; @@ -339,15 +407,19 @@ class cservice : public xClient /* XQ handlers. */ bool doXQLogin(iServer* , const string&, const string&); bool doXQIsCheck(iServer*, const string&, const string&, const string&); + bool doXQSASL(iServer* , const string&, const string&); /** * Array of sqlUser flags and the corresponding accountFlags. */ - static constexpr std::array< std::pair< sqlUser::flagType, iClient::flagType >, 4 > flagMap = { { + static constexpr std::array< std::pair< sqlUser::flagType, iClient::flagType >, 7 > flagMap = { { { sqlUser::F_TOTP_ENABLED, iClient::X_TOTP_ENABLED }, { sqlUser::F_TOTP_REQ_IPR, iClient::X_TOTP_REQ_IPR }, { sqlUser::F_GLOBAL_SUSPEND, iClient::X_GLOBAL_SUSPEND }, - { sqlUser::F_FRAUD, iClient::X_FRAUD } + { sqlUser::F_FRAUD, iClient::X_FRAUD }, + { sqlUser::F_CERTONLY, iClient::X_CERTONLY }, + { sqlUser::F_CERT_DISABLE_TOTP, iClient::X_CERT_DISABLE_TOTP }, + { sqlUser::F_WEB_DISABLE_TOTP, iClient::X_WEB_DISABLE_TOTP } } } ; /** @@ -369,7 +441,7 @@ class cservice : public xClient CONFIG_VAR_LIST #undef CONFIG_VAR - enum { + enum AuthResult { AUTH_SUCCEEDED, AUTH_FAILED, TOO_EARLY_TOLOGIN, @@ -377,10 +449,13 @@ class cservice : public xClient AUTH_SUSPENDED_USER, AUTH_NO_TOKEN, AUTH_INVALID_PASS, + AUTH_INVALID_FINGERPRINT, AUTH_ERROR, AUTH_INVALID_TOKEN, AUTH_FAILED_IPR, - AUTH_ML_EXCEEDED + AUTH_CERTONLY, + AUTH_ML_EXCEEDED, + AUTH_FAIL_EXCEEDED }; cservice(const string& args); @@ -480,6 +555,26 @@ class cservice : public xClient /* Returns what access a user has in the coder channel */ short getCoderAccessLevel( sqlUser* ); + /* Checks if a user has an existing TLS fingerprint for it's username + * Returns the COUNT(*) + */ + unsigned int hasFP( sqlUser* ); + + /* Returns true if the fingerprint exists and matches the user_id. */ + bool checkFP( const string& fingerprint, unsigned int userId ) + { + auto it = fingerprintMap.find( fingerprint ) ; + return ( it != fingerprintMap.end() && it->second == userId ) ; + } + + /* Adds a fingerprint to cache. */ + auto addFP( const string& fingerprint, unsigned int userId ) + { return fingerprintMap.emplace( fingerprint, userId ) ; } + + /* Removes a fingerprint from cache. */ + void removeFP( const string& fingerprint ) + { fingerprintMap.erase( fingerprint ) ; } + /* Fetch a user record from cache only. */ inline std::optional< sqlUser* > getCachedUserRecord( const string& user_name ) { @@ -575,8 +670,21 @@ class cservice : public xClient unsigned int getOutputTotal(const iClient* theClient); bool hasOutputFlooded(iClient*); - int authenticateUser(const string& username, const string& password, const string& ip, const string& ident,unsigned int&, sqlUser**); - int authenticateUser(const string& username, const string& password, iClient*, sqlUser**); + using AuthStruct = struct { + AuthType type ; + unsigned int result ; + std::string username ; + std::string password ; + std::string ident ; + std::string ip ; + std::string fingerprint ; + sqlUser* theUser ; + iClient* theClient ; + SaslMechanism sasl ; + } ; + + AuthResult authenticateUser( AuthStruct& ) ; + bool processAuthentication( AuthStruct, std::string* Message = nullptr ) ; void doXQToAllServices(const string&, const string&); @@ -833,11 +941,13 @@ class cservice : public xClient void preloadChannelCache(); void preloadLevelsCache(); void preloadUserCache(); + void preloadFingerprintCache(); bool loadGlines(); void updateChannels(); void updateUsers(); + void updateFingerprints(); void updateUserLevels(sqlUser* ); void updateLevels(int channelId = 0); vector getChannelManager(int channelId); diff --git a/mod.cservice/cserviceCommands.h b/mod.cservice/cserviceCommands.h index 4d468fb1..8831e1c1 100644 --- a/mod.cservice/cserviceCommands.h +++ b/mod.cservice/cserviceCommands.h @@ -106,6 +106,7 @@ DECLARE_COMMAND( VERIFY ) DECLARE_COMMAND( RANDOM ) DECLARE_COMMAND( SUPPORT ) DECLARE_COMMAND( NOTE ) +DECLARE_COMMAND( CERT ) // Channel user level commands. diff --git a/mod.cservice/cservice_config.h b/mod.cservice/cservice_config.h index 8146afdb..c89ab5ec 100644 --- a/mod.cservice/cservice_config.h +++ b/mod.cservice/cservice_config.h @@ -123,10 +123,27 @@ #undef GLINE_ON_FLOODPRO /* - * Enable this to send new AC messages upon changes of sqlUser flags. + * Enable this to enable the following features currently not released in ircu: + * - Send new AC messages upon changes of sqlUser flags. + * - TLS connections + * - SASL authentication + * - Network config + * - Autohide hostmask (+x) * All servers on the network must run the appropriate version of ircu * in order not to get protocol violation messages. */ -#undef USE_AC_XFLAGS +#undef NEW_IRCU_FEATURES + +/** + * Define the number of iterations for the SCRAM record generation. + * Must not exceed 4096. + */ +#define CRYPT_ITERATIONS 4096 + +/** + * Define the length of the salt for the SCRAM record generation. + * Must be in the range of 12 to 64 bytes. + */ +#define CRYPT_SALT_LEN 16 #endif // __CSERVICE_CONFIG_H diff --git a/mod.cservice/cservice_confvars.h b/mod.cservice/cservice_confvars.h index b1890554..0a7f330b 100644 --- a/mod.cservice/cservice_confvars.h +++ b/mod.cservice/cservice_confvars.h @@ -81,6 +81,8 @@ CONFIG_VAR( unsigned int, preloadUserDays, "preload_user_days") \ CONFIG_VAR( unsigned int, partIdleChan, "part_idle_chan") \ CONFIG_VAR( unsigned int, connectRetry, "connection_retry_total") \ + CONFIG_VAR( unsigned int, saslTimeout, "sasl_timeout") \ + CONFIG_VAR( unsigned int, maxFingerprints, "max_fingerprints") \ CONFIG_VAR( unsigned int, logVerbosity, "log_verbosity") \ CONFIG_VAR( unsigned int, chanVerbosity, "chan_verbosity") \ CONFIG_VAR( unsigned int, consoleVerbosity, "console_verbosity") \ diff --git a/mod.cservice/cservice_crypt.cc b/mod.cservice/cservice_crypt.cc new file mode 100644 index 00000000..b5fc6990 --- /dev/null +++ b/mod.cservice/cservice_crypt.cc @@ -0,0 +1,345 @@ +/** + * cservice_crypt.cc + * Author: MrIron + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + * + */ + +#include "cservice_crypt.h" +#ifdef HAVE_LIBSSL + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "cservice_config.h" +#include "ELog.h" + +namespace gnuworld +{ + +/** + * Write an error if `err` is non-null, then return std::nullopt. + */ +template +static std::optional fail( std::string_view msg, std::string* err ) +{ + if( err ) *err = std::string( msg ) ; + return std::nullopt ; +} + +/** + * Base64-encode a byte array. + */ +std::string b64encode( const unsigned char* data, size_t len ) +{ + std::string out ; + out.resize( 4 * ( ( len + 2 ) / 3 ) ) ; + int n = EVP_EncodeBlock( reinterpret_cast< unsigned char* >( &out[ 0 ] ), data, (int)len ) ; + if( n < 0 ) return {} ; + out.resize( (size_t)n ) ; + return out ; +} + +/** + * Base64-decode a string to a byte vector. + */ +std::optional< std::vector< unsigned char > > +b64decode( std::string_view b64, std::string* err, bool validatePrintable ) +{ + if( b64.size() % 4 != 0 ) return fail< std::vector< unsigned char > >( "bad base64 length", err ) ; + + // Validate that the input contains only valid base64 characters + for( char c : b64 ) { + if( !std::isalnum( c ) && c != '+' && c != '/' && c != '=' ) { + return fail< std::vector< unsigned char > >( "invalid base64 character", err ) ; + } + } + + std::vector< unsigned char > out ; + out.resize( 3 * ( b64.size() / 4 ) ) ; + + int n = EVP_DecodeBlock( out.data(), + reinterpret_cast< const unsigned char* >( b64.data() ), + (int)b64.size() ) ; + if( n < 0 ) return fail< std::vector< unsigned char > >( "bad base64", err ) ; + + // Compute real length by subtracting '=' padding (0,1,2) + size_t pad = 0 ; + if( !b64.empty() && b64.back() == '=' ) + { + pad++ ; + if( b64.size() >= 2 && b64[ b64.size() - 2 ] == '=' ) pad++ ; + } + size_t real_len = (size_t)n - pad ; + if( real_len > out.size() ) return fail< std::vector< unsigned char > >( "base64 overflow", err ) ; + out.resize( real_len ) ; + + // Optionally validate that decoded data contains only printable ASCII characters and null bytes + if( validatePrintable ) { + for( size_t i = 0 ; i < real_len ; ++i ) { + if( out[ i ] != 0 && ( out[ i ] < 32 || out[ i ] > 126 ) ) { + return fail< std::vector< unsigned char > >( "decoded data contains non-printable characters", err ) ; + } + } + } + + return out ; +} + +/** + * PBKDF2-HMAC-SHA256 key derivation. + */ +static inline std::optional< std::array< unsigned char, 32 > > +pbkdf2_sha256( std::string_view password, + const unsigned char* salt, size_t salt_len, + int iterations, + std::string* err ) +{ + std::array< unsigned char, 32 > out{} ; + if( !PKCS5_PBKDF2_HMAC( password.data(), (int)password.size(), + salt, (int)salt_len, iterations, + EVP_sha256(), (int)out.size(), out.data() ) ) + return fail< std::array< unsigned char, 32 > >( "PBKDF2 failed", err ) ; + return out ; +} + +/** + * SHA256 hash. + */ +static inline std::optional< std::array< unsigned char, 32 > > +sha256( const unsigned char* data, size_t len, std::string* err ) +{ + std::array< unsigned char, 32 > out{} ; + unsigned int outlen = 0 ; + EVP_MD_CTX* ctx = EVP_MD_CTX_new() ; + if( !ctx ) return fail< std::array< unsigned char, 32 > >( "EVP_MD_CTX_new failed", err ) ; + if( EVP_DigestInit_ex( ctx, EVP_sha256(), nullptr ) != 1 + || EVP_DigestUpdate( ctx, data, len ) != 1 + || EVP_DigestFinal_ex( ctx, out.data(), &outlen ) != 1 ) + { + EVP_MD_CTX_free( ctx ) ; + return fail< std::array< unsigned char, 32 > >( "SHA256 failed", err ) ; + } + EVP_MD_CTX_free( ctx ) ; + return out ; +} + +/** + * HMAC-SHA256. + */ +static inline std::optional< std::array< unsigned char, 32 > > +hmac_sha256( const unsigned char* key, size_t key_len, + const unsigned char* msg, size_t msg_len, + std::string* err ) +{ + std::array< unsigned char, 32 > out{} ; + unsigned int outlen = 0 ; + if( !HMAC( EVP_sha256(), key, (int)key_len, msg, msg_len, out.data(), &outlen ) ) + return fail< std::array< unsigned char, 32 > >( "HMAC failed", err ) ; + return out ; +} + +/** + * Parse a SCRAM-SHA-256 record string into its components. + */ +std::optional< ScramParsed > +parse_scram_sha256_record( const std::string& rec, std::string* err ) +{ + constexpr const char* kPrefix = "SCRAM-SHA-256$" ; + if( rec.rfind( kPrefix, 0 ) != 0 ) + return fail< ScramParsed >( "bad prefix", err ) ; + + std::string_view rest( rec ) ; + rest.remove_prefix( std::char_traits< char >::length( kPrefix ) ) ; + + // iterations + auto pos1 = rest.find( ':' ) ; + if( pos1 == std::string_view::npos ) return fail< ScramParsed >( "bad record (no ':')", err ) ; + int iterations = 0 ; + { + auto it_str = rest.substr( 0, pos1 ) ; + auto first = it_str.data() ; + auto last = it_str.data() + it_str.size() ; + auto res = std::from_chars( first, last, iterations ) ; + if( res.ec != std::errc() || res.ptr != last ) + return fail< ScramParsed >( "bad iterations", err ) ; + } + rest.remove_prefix( pos1 + 1 ) ; + + // salt_b64 + auto pos2 = rest.find( '$' ) ; + if( pos2 == std::string_view::npos ) return fail< ScramParsed >( "bad record (no '$')", err ) ; + auto salt_b64 = rest.substr( 0, pos2 ) ; + rest.remove_prefix( pos2 + 1 ) ; + + // stored_b64 : server_b64 + auto pos3 = rest.find( ':' ) ; + if( pos3 == std::string_view::npos ) return fail< ScramParsed >( "bad record (no second ':')", err ) ; + auto stored_b64 = rest.substr( 0, pos3 ) ; + auto server_b64 = rest.substr( pos3 + 1 ) ; + + auto saltOpt = b64decode( salt_b64, err ) ; + if( !saltOpt ) return std::nullopt ; + auto storedOpt = b64decode( stored_b64, err ) ; + if( !storedOpt ) return std::nullopt ; + auto serverOpt = b64decode( server_b64, err ) ; + if( !serverOpt ) return std::nullopt ; + + if( storedOpt->size() != 32 ) return fail< ScramParsed >( "storedKey wrong size", err ) ; + if( serverOpt->size() != 32 ) return fail< ScramParsed >( "serverKey wrong size", err ) ; + + ScramParsed out { + iterations, + std::move( *saltOpt ), + std::move( *storedOpt ), + std::move( *serverOpt ) + } ; + return out ; +} + +/** + * Create a SCRAM-SHA-256 record string from a plaintext password. + */ +std::optional< std::string > +make_scram_sha256_record( std::string_view password, std::string* err ) +{ + if( CRYPT_ITERATIONS < 4096 ) + return fail< std::string >( "iterations too low", err ) ; + if( CRYPT_SALT_LEN < 12 || CRYPT_SALT_LEN > 64 ) + return fail< std::string >( "salt_len out of range", err ) ; + + std::vector< unsigned char > salt( CRYPT_SALT_LEN ) ; + if( RAND_bytes( salt.data(), (int)salt.size() ) != 1 ) + return fail< std::string >( "RAND_bytes failed", err ) ; + + auto saltedOpt = pbkdf2_sha256( password, salt.data(), salt.size(), CRYPT_ITERATIONS, err ) ; + if( !saltedOpt ) return std::nullopt ; + auto& salted = *saltedOpt ; + + static const unsigned char ck[] = "Client Key" ; + auto clientKeyOpt = hmac_sha256( salted.data(), salted.size(), ck, sizeof( ck ) - 1, err ) ; + if( !clientKeyOpt ) return std::nullopt ; + auto& clientKey = *clientKeyOpt ; + + auto storedKeyOpt = sha256( clientKey.data(), clientKey.size(), err ) ; + if( !storedKeyOpt ) return std::nullopt ; + auto& storedKey = *storedKeyOpt ; + + static const unsigned char sk[] = "Server Key" ; + auto serverKeyOpt = hmac_sha256( salted.data(), salted.size(), sk, sizeof( sk ) - 1, err ) ; + if( !serverKeyOpt ) return std::nullopt ; + auto& serverKey = *serverKeyOpt ; + + std::string rec ; + rec.reserve( 64 + CRYPT_SALT_LEN ) ; + rec += "SCRAM-SHA-256$" ; + rec += std::to_string( CRYPT_ITERATIONS ) ; + rec += ":" ; + rec += b64encode( salt.data(), salt.size() ) ; + rec += "$" ; + rec += b64encode( storedKey.data(), storedKey.size() ) ; + rec += ":" ; + rec += b64encode( serverKey.data(), serverKey.size() ) ; + + OPENSSL_cleanse( salted.data(), salted.size() ) ; + OPENSSL_cleanse( clientKey.data(), clientKey.size() ) ; + + return rec ; +} + +/** + * Generate a random nonce for SCRAM authentication. + */ +std::optional< std::string > generateRandomNonce( size_t length, std::string* err ) +{ + std::vector< unsigned char > buf( length ) ; + if( RAND_bytes( buf.data(), (int)length ) != 1 ) + return fail< std::string >( "RAND_bytes failed in generateRandomNonce", err ) ; + // Use base64 for nonce encoding, remove any trailing '=' + std::string nonce = b64encode( buf.data(), buf.size() ) ; + nonce.erase( std::remove( nonce.begin(), nonce.end(), '=' ), nonce.end() ) ; + return nonce ; +} + +/** + * Validate a SCRAM-SHA-256 client proof. + */ +bool validate_scram_sha256_proof( + const std::vector< unsigned char >& storedKey, + const std::string& authMessage, + const std::string& clientProof_b64 +) +{ + auto proofOpt = b64decode( clientProof_b64 ) ; + if( !proofOpt || proofOpt->size() != storedKey.size() ) + { + elog << "[SCRAM] Proof decode failed or wrong size\n" ; + return false ; + } + const std::vector< unsigned char >& clientProof = *proofOpt ; + + unsigned int siglen = 0 ; + unsigned char clientSignature[ SHA256_DIGEST_LENGTH ] ; + HMAC( EVP_sha256(), + storedKey.data(), storedKey.size(), + reinterpret_cast< const unsigned char* >( authMessage.data() ), authMessage.size(), + clientSignature, &siglen ) ; + + std::vector< unsigned char > clientKey( clientProof.size() ) ; + for( size_t i = 0 ; i < clientProof.size() ; ++i ) + clientKey[ i ] = clientProof[ i ] ^ clientSignature[ i ] ; + + unsigned char computedStoredKey[ SHA256_DIGEST_LENGTH ] ; + SHA256( clientKey.data(), clientKey.size(), computedStoredKey ) ; + + bool result = std::memcmp( computedStoredKey, storedKey.data(), SHA256_DIGEST_LENGTH ) == 0 ; + return result ; +} + +/** + * Compute the SCRAM server signature. + */ +std::string compute_server_signature( + const std::vector< unsigned char >& serverKey, + const std::string& authMessage +) +{ + unsigned int siglen = 0 ; + unsigned char serverSignature[ SHA256_DIGEST_LENGTH ] ; + HMAC( EVP_sha256(), + serverKey.data(), serverKey.size(), + reinterpret_cast< const unsigned char* >( authMessage.data() ), authMessage.size(), + serverSignature, &siglen ) ; + return b64encode( serverSignature, siglen ) ; +} + +} // namespace gnuworld + +#endif // HAVE_LIBSSL diff --git a/mod.cservice/cservice_crypt.h b/mod.cservice/cservice_crypt.h new file mode 100644 index 00000000..56ebbea5 --- /dev/null +++ b/mod.cservice/cservice_crypt.h @@ -0,0 +1,92 @@ +/** + * cservice_crypt.h + * Author: MrIron + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + * + */ + +#include "defs.h" +#ifdef HAVE_LIBSSL + +#pragma once + +#include +#include +#include + +namespace gnuworld { + +/** + * Parsed SCRAM-SHA-256 record. + */ +struct ScramParsed { + int iterations ; + std::vector< unsigned char > salt ; // variable length, typically 16 + std::vector< unsigned char > storedKey ; // 32 bytes + std::vector< unsigned char > serverKey ; // 32 bytes +} ; + +/** + * Create a SCRAM-SHA-256 record string from a plaintext password. + * Format: "SCRAM-SHA-256$:$:" + * Returns std::nullopt on failure; writes error text into err if provided. + */ +std::optional< std::string > make_scram_sha256_record( std::string_view password, std::string* err = nullptr ) ; + +/** + * Parse a SCRAM-SHA-256 record string into its components. + * Returns std::nullopt on parse error; writes error text into err if provided. + */ +std::optional< ScramParsed > parse_scram_sha256_record( const std::string& record, std::string* err = nullptr ) ; + +/** + * Generate a random nonce for SCRAM authentication. + * Returns std::nullopt on failure; writes error text into err if provided. + */ +std::optional< std::string > generateRandomNonce( size_t length = 18, std::string* err = nullptr ) ; + +/** + * Base64-encode a byte array. + */ +std::string b64encode( const unsigned char* data, size_t len ) ; + +/** + * Base64-decode a byte array. + */ +std::optional< std::vector< unsigned char > > +b64decode( std::string_view b64, std::string* err = nullptr, bool validatePrintable = false ) ; + +/** + * Validate a SCRAM-SHA-256 client proof. + */ +bool validate_scram_sha256_proof( + const std::vector< unsigned char >& storedKey, + const std::string& authMessage, + const std::string& clientProof_b64 +) ; + +/** + * Compute the SCRAM server signature. + */ +std::string compute_server_signature( + const std::vector< unsigned char >& serverKey, + const std::string& authMessage +) ; + +} // namespace + +#endif // HAVE_LIBSSL diff --git a/mod.cservice/responses.h b/mod.cservice/responses.h index 109097ce..8c8f00fd 100644 --- a/mod.cservice/responses.h +++ b/mod.cservice/responses.h @@ -230,7 +230,10 @@ namespace gnuworld const int reason_must = 192; const int susp_reason = 193; const int unsusp_reason = 194; - + const int certonly_on = 195; + const int certonly_off = 196; + const int autohide_on = 197; + const int autohide_off = 198; // Allow for merge of other features const int welcome_max_len = 205; @@ -256,6 +259,17 @@ namespace gnuworld const int mode_wrongkey = 223; const int mode_keylength = 224; + const int no_fingerprints_registered = 225; + const int no_fingerprints_found = 226; + const int your_fingerprint_is = 227; + const int max_fingerprints = 228; + const int invalid_fingerprint = 229; + const int fingerprint_already_exists = 230; + const int fingerprint_added = 231; + const int fingerprint_removed = 232; + const int fingerprint_norem_certonly = 233; + const int fingerprint_not_found = 234; + const int greeting = 9998; const int motd = 9999; } diff --git a/mod.cservice/sqlUser.cc b/mod.cservice/sqlUser.cc index 597964b7..63623906 100644 --- a/mod.cservice/sqlUser.cc +++ b/mod.cservice/sqlUser.cc @@ -68,6 +68,7 @@ sqlUser::sqlUser(cservice* _bot) notes_sent(0), failed_logins(0), failed_login_ts(0), + scram_record(), logger(_bot->getLogger()), SQLDb(_bot->SQLDb) { @@ -171,22 +172,25 @@ verifdata = SQLDb->GetValue(row, 11); failed_logins = 0; failed_login_ts = 0; totp_key = SQLDb->GetValue(row, 12); +scram_record = SQLDb->GetValue(row, 13); /* Fetch the "Last Seen" time from the users_lastseen table. */ } bool sqlUser::commit(iClient* who) { +if(who) + return commit(who->getNickUserHost()); +else + return commit("Marvin, the paranoid android."); +} + +bool sqlUser::commit(std::string last_updated_by) +{ /* * Build an SQL statement to commit the transient data in this storage class * back into the database. */ -if(who) -{ - last_updated_by = who->getNickUserHost(); -} else { - last_updated_by = "Marvin, the paranoid android."; -} static const char* queryHeader = "UPDATE users "; static const char* queryCondition = "WHERE id = "; @@ -199,7 +203,8 @@ queryString << queryHeader << "maxlogins = " << maxlogins << ", " << "last_updated = date_part('epoch', CURRENT_TIMESTAMP)::int, " << "last_updated_by = '" << escapeSQLChars(last_updated_by) << "', " - << "totp_key = '" << escapeSQLChars(totp_key) << "' " + << "totp_key = '" << escapeSQLChars(totp_key) << "', " + << "scram_record = '" << escapeSQLChars(scram_record) << "' " << queryCondition << id << ends; @@ -424,7 +429,7 @@ bool sqlUser::Insert() */ static const char* queryHeader = "INSERT INTO users " "(user_name,password,language_id,flags,last_updated_by,last_" - "updated,post_forms,signup_ts,email) VALUES ('"; + "updated,post_forms,signup_ts,email,scram_record) VALUES ('"; stringstream queryString; queryString << queryHeader @@ -439,6 +444,7 @@ queryString << queryHeader << "(date_part('epoch', CURRENT_TIMESTAMP)::int + 432000)," << "date_part('epoch', CURRENT_TIMESTAMP)::int,'" << escapeSQLChars(email) + << "','" << escapeSQLChars(scram_record) << "')" << ends; diff --git a/mod.cservice/sqlUser.h b/mod.cservice/sqlUser.h index 19cf36a0..448aedb3 100644 --- a/mod.cservice/sqlUser.h +++ b/mod.cservice/sqlUser.h @@ -42,7 +42,7 @@ class sqlUser sqlUser(cservice*) ; virtual ~sqlUser() ; - typedef unsigned short int flagType ; + typedef unsigned int flagType ; static constexpr flagType F_GLOBAL_SUSPEND = 0x01 ; static constexpr flagType F_LOGGEDIN = 0x02 ; // Deprecated static constexpr flagType F_INVIS = 0x04 ; @@ -55,7 +55,10 @@ class sqlUser static constexpr flagType F_NOADDUSER = 0x200 ; static constexpr flagType F_TOTP_ENABLED = 0x400 ; static constexpr flagType F_TOTP_REQ_IPR = 0x800 ; - + static constexpr flagType F_CERTONLY = 0x1000 ; + static constexpr flagType F_CERT_DISABLE_TOTP = 0x2000 ; + static constexpr flagType F_WEB_DISABLE_TOTP = 0x4000 ; + static constexpr flagType F_AUTOHIDE = 0x8000 ; /* * User 'Event' Flags, used in the userlog table. */ @@ -152,6 +155,9 @@ class sqlUser inline const std::string& getTotpKey() const { return totp_key ; } + inline const std::string& getScramRecord() const + { return scram_record ; } + /* * Methods to set data atrributes. */ @@ -220,12 +226,17 @@ class sqlUser inline void setTotpKey( const std::string& _totp_key ) { totp_key = _totp_key ; } + + inline void setScramRecord( const std::string& _scram_record ) + { scram_record = _scram_record ; } + /* * Method to perform a SQL 'UPDATE' and commit changes to this * object back to the database. */ - bool commit(iClient* who); + bool commit(iClient*); + bool commit(std::string); bool commitLastSeen(); bool commitLastSeenWithoutMask(); time_t getLastSeen(); @@ -269,6 +280,7 @@ class sqlUser unsigned int failed_logins; unsigned int failed_login_ts; std::string totp_key; + std::string scram_record; Logger* logger; dbHandle* SQLDb; diff --git a/mod.dronescan/FAKECommand.cc b/mod.dronescan/FAKECommand.cc index ac36cb08..2430ae04 100644 --- a/mod.dronescan/FAKECommand.cc +++ b/mod.dronescan/FAKECommand.cc @@ -78,6 +78,7 @@ void FAKECommand::Exec( const iClient *theClient, const string& Message, const s string(), 0, 0, + string(), theFake->getRealName(), ::time(0) ); diff --git a/mod.gnutest/gnutest.cc b/mod.gnutest/gnutest.cc index a21a2168..2cb6890e 100644 --- a/mod.gnutest/gnutest.cc +++ b/mod.gnutest/gnutest.cc @@ -867,6 +867,7 @@ iClient* newClient = new (std::nothrow) iClient( string(), // account 0, // account_id 0, // account_flags + string(), // tls fingerprint "test spawn client, moo", // description 31337 // connect time ) ; diff --git a/mod.nickserv/nickserv.cc b/mod.nickserv/nickserv.cc index ec3ba11e..c14dddbe 100644 --- a/mod.nickserv/nickserv.cc +++ b/mod.nickserv/nickserv.cc @@ -607,6 +607,7 @@ for( vector::iterator itr = jupeQueue.begin(); string(), 0, 0, + string(), "Juped Nick", ::time( 0 ) ); diff --git a/mod.scanner/wingateModule.cc b/mod.scanner/wingateModule.cc index 8ef45d31..c701d208 100644 --- a/mod.scanner/wingateModule.cc +++ b/mod.scanner/wingateModule.cc @@ -66,7 +66,7 @@ wingateModule::~wingateModule() void wingateModule::CheckIP( const string& ip ) { -cm->Connect( this, ip, 23 ) ; +cm->Connect( this, ip, 23, false ) ; } void wingateModule::OnConnect( Connection* ) diff --git a/mod.snoop/snoop.cc b/mod.snoop/snoop.cc index fb766919..055cb094 100644 --- a/mod.snoop/snoop.cc +++ b/mod.snoop/snoop.cc @@ -197,6 +197,7 @@ iClient* newClient = new (std::nothrow) iClient( string(), // account 0, // account_id 0, // account_flags + string(), // tls fingerprint realname, 31337 // connect time ) ; diff --git a/src/Channel.cc b/src/Channel.cc index 1f1bee5d..cb79ec3e 100644 --- a/src/Channel.cc +++ b/src/Channel.cc @@ -59,6 +59,7 @@ const Channel::modeType Channel::MODE_C = 0x02000 ; const Channel::modeType Channel::MODE_CTCP = 0x04000 ; const Channel::modeType Channel::MODE_PART = 0x08000 ; const Channel::modeType Channel::MODE_MNOREG = 0x10000 ; +const Channel::modeType Channel::MODE_Z = 0x20000 ; Channel::Channel( const string& _name, const time_t& _creationTime ) @@ -500,6 +501,7 @@ if( modes & MODE_C ) modeString += 'c'; if( modes & MODE_CTCP ) modeString += 'C'; if( modes & MODE_PART ) modeString += 'u'; if( modes & MODE_MNOREG ) modeString += 'M'; +if( modes & MODE_Z ) modeString += 'Z'; if( modes & MODE_K ) { diff --git a/src/Network.cc b/src/Network.cc index 336d4b11..dee7e9cf 100644 --- a/src/Network.cc +++ b/src/Network.cc @@ -174,6 +174,14 @@ return channelMap.insert( theChan ) ).second ; } +std::optional< std::pair< std::string, time_t > > xNetwork::findNetConf( const std::string& key ) const +{ +auto it = netConfMap.find( key ) ; +if( it != netConfMap.end() ) + return it->second; +return std::nullopt; +} + iClient* xNetwork::findClient( const unsigned int& intYYXXX ) const { numericMapType::const_iterator ptr = numericMap.find( intYYXXX ) ; diff --git a/src/client.cc b/src/client.cc index ee4334b2..f033a8f2 100644 --- a/src/client.cc +++ b/src/client.cc @@ -122,6 +122,7 @@ if( mode & iClient::MODE_SERVICES ) Mode += 'k' ; if( mode & iClient::MODE_OPER ) Mode += 'o' ; if( mode & iClient::MODE_WALLOPS ) Mode += 'w' ; if( mode & iClient::MODE_INVISIBLE ) Mode += 'i' ; +if( mode & iClient::MODE_TLS ) Mode += 'z' ; return Mode ; } @@ -153,6 +154,7 @@ for( ; ptr != end ; ++ptr ) case 'o': mode |= iClient::MODE_OPER ; break; case 'w': mode |= iClient::MODE_WALLOPS ; break; case 'i': mode |= iClient::MODE_INVISIBLE ; break; + case 'z': mode |= iClient::MODE_TLS ; break; default: elog << "xClient::Mode> Unknown mode: " @@ -2667,11 +2669,16 @@ for( string::size_type modePos = 0 ; modePos < modes.size() ; ++modePos ) modeVector.push_back(make_pair(false, Channel::MODE_PART)); break; - case 'M': // mode to prevent part messages + case 'M': // mode to moderate for non-authed users theChan->removeMode(Channel::MODE_MNOREG); modeVector.push_back(make_pair(false, Channel::MODE_MNOREG)); break; + case 'Z': // TLS only? + theChan->removeMode(Channel::MODE_Z); + modeVector.push_back(make_pair(false, + Channel::MODE_Z)); + break; case 'A': // Apass for oplevels if (theChan->getMode(Channel::MODE_A)) { diff --git a/src/iClient.cc b/src/iClient.cc index 7af8ec13..310cb345 100644 --- a/src/iClient.cc +++ b/src/iClient.cc @@ -55,6 +55,7 @@ iClient::iClient( const unsigned int& /* _uplink */, const string& _account, const unsigned int _account_id, const flagType _account_flags, + const string& _tls_fingerprint, const string& _description, const time_t& _nick_ts ) : NetworkTarget( _yyxxx ), @@ -69,7 +70,8 @@ iClient::iClient( const unsigned int& /* _uplink */, mode( 0 ), account( _account ), account_id( _account_id ), - account_flags( _account_flags ) + account_flags( _account_flags ), + tlsFingerprint( _tls_fingerprint ) { setModes( _mode ) ; customDataMap = 0 ; @@ -87,6 +89,7 @@ iClient::iClient( const unsigned int& /* _uplink */, const string& _account, const unsigned int _account_id, const flagType _account_flags, + const string& _tls_fingerprint, const string& _setHost, const string& _fakeHost, const string& _description, @@ -103,7 +106,8 @@ iClient::iClient( const unsigned int& /* _uplink */, mode( 0 ), account( _account ), account_id( _account_id ), - account_flags( _account_flags ) + account_flags( _account_flags ), + tlsFingerprint( _tls_fingerprint ) #ifdef ASUKA ,setHost( _setHost ) #endif @@ -161,6 +165,9 @@ for( string::size_type i = 0 ; i < newModes.size() ; i++ ) case 'g': setModeG() ; break ; + case 'z': + setModeZ() ; + break ; case 'r': case 'R': setModeR() ; @@ -221,6 +228,7 @@ if( isModeK() ) retMe += 'k' ; if( isModeR() ) retMe += 'r' ; if( isModeX() ) retMe += 'x' ; if( isModeG() ) retMe += 'g' ; +if( isModeZ() ) retMe += 'z' ; return retMe ; } diff --git a/src/main.cc b/src/main.cc index 3a42ebca..ddc0742a 100644 --- a/src/main.cc +++ b/src/main.cc @@ -249,6 +249,7 @@ xServer::xServer( bool verbose_arg, bool doDebug_arg, bool logSocket_arg, const std::string& configFileName_arg, const std::string& simFileName_arg ) : eventList( EVT_NOOP ), + tlsEnabled( false ), verbose( verbose_arg ), doDebug( doDebug_arg ), logSocket( logSocket_arg ), @@ -310,7 +311,7 @@ while( keepRunning ) << "... " << endl ; - serverConnection = Connect( this, UplinkName, Port ) ; + serverConnection = Connect( this, UplinkName, Port, tlsEnabled ) ; } } // if( NULL == serverConnection ) diff --git a/src/server.cc b/src/server.cc index d802097e..476c2a5e 100644 --- a/src/server.cc +++ b/src/server.cc @@ -264,9 +264,70 @@ glineUpdateInterval = static_cast< time_t >( atoi( pingUpdateInterval = static_cast< time_t >( atoi( conf.Require( "pingupdateinterval" )->second.c_str() ) ) ; +// Check TLS configuration settings +#ifdef HAVE_LIBSSL +EConfig::const_iterator tlsItr = conf.Find( "tls" ) ; +if ( tlsItr == conf.end() || tlsItr->second != "yes" ) +{ + tlsEnabled = false ; +} else { + tlsEnabled = true ; + + // If TLS is enabled, parse the other configuration options + tlsKeyFile = conf.Require( "tlsKeyFile" )->second ; + tlsCertFile = conf.Require( "tlsCertFile" )->second ; + + if (!initTls()) { + elog << "TLS initialization error. Exiting." << endl; + ::exit(1); + } +} +#endif return true ; } +/** + * This function is only called if TLS is enabled in config. We exit if gnuworld + * is not compiled with TLS support. + */ +#ifdef HAVE_LIBSSL +bool xServer::initTls() +{ + elog << "xServer::initTls - Spinning up TLS" << endl; + SSL_library_init(); + SSL_load_error_strings(); + sslCtx = SSL_CTX_new(TLS_method()); + + SSL_CTX_set_min_proto_version(sslCtx, TLS1_2_VERSION); // Allow TLS 1.2 and above + SSL_CTX_set_max_proto_version(sslCtx, TLS1_3_VERSION); // Allow up to TLS 1.3 + SSL_CTX_set_verify(sslCtx, SSL_VERIFY_NONE, NULL); + + int res = SSL_CTX_use_certificate_chain_file(sslCtx, tlsCertFile.c_str()); + if (res != 1) { + elog << "xServer::initTls - Could not load certificate file" << endl; + SSL_CTX_free(sslCtx); + return false; + } + + res = SSL_CTX_use_PrivateKey_file(sslCtx, tlsKeyFile.c_str(), SSL_FILETYPE_PEM); + if (res != 1) { + elog << "xServer::initTls - Could not load key file" << endl; + SSL_CTX_free(sslCtx); + return false; + } + + res = SSL_CTX_check_private_key(sslCtx); + if (res != 1) { + elog << "xServer::initTls - Private key validation failed" << endl; + SSL_CTX_free(sslCtx); + return false; + } + + SSL_CTX_set_cipher_list(sslCtx, SSL_DEFAULT_CIPHER_LIST); + return true; +} +#endif + bool xServer::loadCommandHandlers() { std::ifstream commandMapFile( commandMapFileName.c_str() ) ; @@ -953,6 +1014,7 @@ iClient* theIClient = new (std::nothrow) iClient( 0, string(), string(), + string(), Client->getDescription(), ::time( 0 ) ) ; assert( theIClient != 0 ) ; @@ -1805,6 +1867,12 @@ if( !chanModes.empty() && else theChan->removeMode(Channel::MODE_MNOREG); break; + case 'Z': + if (plus) + theChan->setMode(Channel::MODE_Z); + else + theChan->removeMode(Channel::MODE_Z); + break; // TODO: Finish with polarity // TODO: Add in support for modes b,v,o @@ -1951,8 +2019,12 @@ s << theClient->getCharYY() << " N " << hopCount << " 31337 " << theClient->getUserName() << ' ' << theClient->getHostName() << ' ' - << theClient->getModes() << ' ' - << "AAAAAA " + << theClient->getModes() << ' ' ; + +if( theClient->getMode( iClient::MODE_TLS ) ) + s << "_ " ; + +s << "AAAAAA " << theClient->getCharYYXXX() << " :" << theClient->getDescription() ; Write( s ) ; @@ -2138,6 +2210,10 @@ if( theChan->getMode( Channel::MODE_MNOREG ) ) { modeVector.push_back( make_pair( false, Channel::MODE_MNOREG ) ) ; } +if( theChan->getMode( Channel::MODE_Z ) ) + { + modeVector.push_back( make_pair( false, Channel::MODE_Z ) ) ; + } if( theChan->getMode( Channel::MODE_L ) ) { OnChannelModeL( theChan, false, 0, 0 ) ; @@ -2303,6 +2379,7 @@ chanModes[ 'c' ] = Channel::MODE_C ; chanModes[ 'C' ] = Channel::MODE_CTCP ; chanModes[ 'u' ] = Channel::MODE_PART ; chanModes[ 'M' ] = Channel::MODE_MNOREG ; +chanModes[ 'Z' ] = Channel::MODE_Z ; // This vector is used for argument-less types that can be passed // to OnChannelMode() @@ -2379,6 +2456,7 @@ for( ; tokenIndex < st.size() ; ) case 'u': case 'M': case 'D': + case 'Z': // elog << "xServer::Mode> General mode: " // << theChar // << ", polarity: " @@ -3370,6 +3448,9 @@ for( string::const_iterator ptr = st[ 0 ].begin() ; ptr != st[ 0 ].end() ; case 'M': theChan->setMode( Channel::MODE_MNOREG ) ; break; + case 'Z': + theChan->setMode( Channel::MODE_Z ) ; + break; case 'k': { if( argPos >= st.size() ) diff --git a/src/server_connection.cc b/src/server_connection.cc index 0f3a0c0c..6186f879 100644 --- a/src/server_connection.cc +++ b/src/server_connection.cc @@ -50,7 +50,6 @@ //#include "events.h" //#include "ip.h" -#include "misc.h" #include "server.h" #include "Network.h" //#include "iServer.h" @@ -64,6 +63,7 @@ //#include "ServerTimerHandlers.h" //#include "LoadClientTimerHandler.h" //#include "UnloadClientTimerHandler.h" +#include "misc.h" #include "ConnectionManager.h" #include "ConnectionHandler.h" #include "Connection.h"