E-MailRelay
gsecretsfile.cpp
Go to the documentation of this file.
1//
2// Copyright (C) 2001-2024 Graeme Walker <graeme_walker@users.sourceforge.net>
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program. If not, see <http://www.gnu.org/licenses/>.
16// ===
17///
18/// \file gsecretsfile.cpp
19///
20
21#include "gdef.h"
22#include "gsecretsfile.h"
23#include "gsecrets.h"
24#include "groot.h"
25#include "gxtext.h"
26#include "gbase64.h"
27#include "gstr.h"
28#include "gstringtoken.h"
29#include "gdatetime.h"
30#include "gfile.h"
31#include "gassert.h"
32#include "glog.h"
33#include <fstream>
34#include <sstream>
35
36GAuth::SecretsFile::SecretsFile( const G::Path & path , bool auto_reread , const std::string & debug_name ) :
37 m_path(path) ,
38 m_auto(auto_reread) ,
39 m_debug_name(debug_name) ,
40 m_file_time(0) ,
41 m_check_time(G::SystemTime::now())
42{
43 m_valid = !path.str().empty() ;
44 if( m_valid )
45 read( path ) ;
46}
47
48void GAuth::SecretsFile::check( const std::string & path , bool with_warnings )
49{
50 if( !path.empty() )
51 {
52 Contents contents = readContents( path ) ;
53 showDiagnostics( contents , path , {} , with_warnings ) ;
54 if( contents.m_errors != 0U )
55 throw Error() ;
56 }
57}
58
60{
61 return m_valid ;
62}
63
64void GAuth::SecretsFile::reread() const
65{
66 (const_cast<SecretsFile*>(this))->reread(0) ;
67}
68
69void GAuth::SecretsFile::reread( int )
70{
71 if( m_auto )
72 {
74 G_DEBUG( "GAuth::SecretsFile::reread: file time checked at " << m_check_time << ": now " << now ) ;
75 if( !now.sameSecond(m_check_time) ) // at most once a second
76 {
77 m_check_time = now ;
78 G::SystemTime t = readFileTime( m_path ) ;
79 G_DEBUG( "GAuth::SecretsFile::reread: current file time " << t << ": saved file time " << m_file_time ) ;
80 if( t != m_file_time )
81 {
82 G_LOG_S( "GAuth::Secrets: re-reading secrets file: " << m_path ) ;
83 read( m_path ) ;
84 }
85 }
86 }
87}
88
89void GAuth::SecretsFile::read( const G::Path & path )
90{
91 m_file_time = readFileTime( path ) ;
92 m_contents = readContents( path ) ;
93 showDiagnostics( m_contents , path , m_debug_name , false ) ;
94}
95
96G::SystemTime GAuth::SecretsFile::readFileTime( const G::Path & path )
97{
98 G::Root claim_root ;
99 return G::File::time( path ) ;
100}
101
102GAuth::SecretsFile::Contents GAuth::SecretsFile::readContents( const G::Path & path )
103{
104 std::unique_ptr<std::ifstream> file ;
105 {
106 G::Root claim_root ;
107 file = std::make_unique<std::ifstream>( path.iopath() ) ;
108 }
109 if( !file->good() )
110 {
111 throw OpenError( path.str() ) ;
112 }
113
114 return readContents( *file ) ;
115}
116
117GAuth::SecretsFile::Contents GAuth::SecretsFile::readContents( std::istream & file )
118{
119 Contents contents ;
120 std::string line ;
121 for( unsigned int line_number = 1U ; file.good() ; ++line_number )
122 {
123 G::Str::readLine( file , line ) ;
124 if( !file )
125 break ;
126
127 G::Str::trim( line , G::Str::ws() ) ;
128 if( !line.empty() && line.at(0U) != '#' )
129 {
130 std::string_view line_sv( line ) ;
131 G::StringTokenView t( line_sv , " \t"_sv ) ;
132 std::string_view w1 = t() ;
133 std::string_view w2 = (++t)() ;
134 std::string_view w3 = (++t)() ;
135 std::string_view w4 = (++t)() ;
136 bool sufficient = t.valid() ;
137 std::string_view w5 = (++t)() ;
138 bool excess = (++t).valid() ;
139 if( !w5.empty() && w5.at(0) == '#' )
140 w5 = std::string_view() , excess = false ;
141
142 if( excess )
143 addWarning( contents , line_number , "too many fields"_sv ) ;
144
145 if( sufficient )
146 processLine( contents , line_number , w1 , w2 , w3 , w4 , w5 ) ;
147 else
148 addError( contents , line_number , "too few fields"_sv ) ;
149 }
150 }
151 return contents ;
152}
153
154void GAuth::SecretsFile::processLine( Contents & contents , unsigned int line_number ,
155 std::string_view side , std::string_view type_in , std::string_view id ,
156 std::string_view secret , std::string_view selector )
157{
158 std::string_view type = canonicalView( G::Str::headView( type_in , ":" , false ) ) ;
159 std::string_view type_decoration = G::Str::tailView( type_in , ":" ) ;
160 bool is_server_side = G::Str::imatch( side , "server"_sv ) ;
161 bool is_client_side = G::Str::imatch( side , "client"_sv ) ;
162
163 if( is_server_side && G::Str::imatch( type , "none"_sv ) )
164 {
165 std::string_view ip_range = id ;
166 std::string_view keyword = secret ;
167 bool inserted = contents.m_trust_map.insert( {G::sv_to_string(ip_range),{G::sv_to_string(keyword),line_number}} ).second ;
168 if( !inserted )
169 addError( contents , line_number , "duplicate server trust address"_sv ) ;
170 }
171 else if( is_client_side && G::Str::imatch(type_in,"plain:b") && id == "="_sv && secret == "="_sv )
172 {
173 contents.m_selectors.insert( {G::sv_to_string(selector),0U} ) ;
174 }
175 else
176 {
177 std::string_view id_encoding ;
178 std::string_view secret_encoding ;
179 std::string_view hash_function ;
180 if( G::Str::imatch( type.substr(0U,5U) , "plain" ) )
181 {
182 id_encoding = G::Str::imatch( type_decoration , "b" ) ? "base64"_sv : "xtext"_sv ; // should also allow plain:xb etc
183 secret_encoding = id_encoding ;
184 //hash_function = "" ;
185 }
186 else if( G::Str::imatch( type , "md5"_sv ) && Secret::isDotted(secret) )
187 {
188 id_encoding = "xtext"_sv ;
189 secret_encoding = "dotted"_sv ;
190 hash_function = "md5"_sv ;
191 }
192 else
193 {
194 id_encoding = "xtext"_sv ;
195 secret_encoding = "base64"_sv ;
196 hash_function = type ;
197 }
198
199 if( is_server_side )
200 {
201 std::string key = serverKey( type , Secret::decode({id,id_encoding}) ) ;
202 Secret secret_obj( {id,id_encoding} , {secret,secret_encoding} , hash_function , lineContext(line_number) ) ;
203 bool inserted = contents.m_map.insert( {key,secret_obj} ).second ;
204 if( inserted )
205 contents.m_server_types.insert( G::Str::lower(type) ) ;
206 else
207 addError( contents , line_number , "duplicate server secret"_sv ) ;
208 }
209 else if( is_client_side )
210 {
211 std::string key = clientKey( type , selector ) ;
212 Secret secret_obj( {id,id_encoding} , {secret,secret_encoding} , hash_function , lineContext(line_number) ) ;
213 bool inserted = contents.m_map.insert( {key,secret_obj} ).second ;
214 if( inserted )
215 ((*(contents.m_selectors.insert( {G::sv_to_string(selector),0U} ).first)).second)++ ;
216 else
217 addError( contents , line_number , "duplicate client secret"_sv ) ;
218 }
219 else
220 {
221 addError( contents , line_number , "invalid value in first field"_sv , side ) ;
222 }
223 }
224}
225
226void GAuth::SecretsFile::addWarning( Contents & contents , unsigned int line_number , std::string_view message , std::string_view more )
227{
228 contents.m_diagnostics.emplace_back( false , line_number , join(message,more) ) ;
229}
230
231void GAuth::SecretsFile::addError( Contents & contents , unsigned int line_number , std::string_view message , std::string_view more )
232{
233 contents.m_diagnostics.emplace_back( true , line_number , join(message,more) ) ;
234 contents.m_errors++ ;
235}
236
237std::string GAuth::SecretsFile::join( std::string_view message_in , std::string_view more )
238{
239 std::string message( message_in.data() , message_in.size() ) ;
240 if( !more.empty() )
241 message.append(": [",3U).append(G::Str::printable(more)).append(1U,']') ;
242 return message ;
243}
244
245void GAuth::SecretsFile::showDiagnostics( const Contents & c , const G::Path & path , const std::string & debug_name , bool with_warnings )
246{
247 if( c.m_diagnostics.empty() )
248 return ;
249
250 if( !with_warnings && c.m_errors == 0U )
251 return ;
252
253 G_WARNING( "GAuth::SecretsFile::read: problems reading" << (debug_name.empty()?"":" ") << debug_name << " "
254 "secrets file [" << path.str() << "]..." ) ;
255
256 std::string prefix = path.basename() ;
257 for( const auto & d : c.m_diagnostics )
258 {
259 if( std::get<0>(d) )
260 G_ERROR( "GAuth::SecretsFile::read: " << prefix << "(" << std::get<1>(d) << "): " << std::get<2>(d) ) ;
261 else if( with_warnings )
262 G_WARNING( "GAuth::SecretsFile::read: " << prefix << "(" << std::get<1>(d) << "): " << std::get<2>(d) ) ;
263 }
264}
265
266std::string_view GAuth::SecretsFile::canonicalView( std::string_view type )
267{
268 // (for backwards compatibility -- new code exects plain, md5, sha1, sha512 etc)
269 if( G::Str::imatch( type , "cram-md5"_sv ) ) return "md5" ;
270 if( G::Str::imatch( type , "apop"_sv ) ) return "md5" ;
271 if( G::Str::imatch( type , "login"_sv ) ) return "plain" ;
272 return type ;
273}
274
275std::string GAuth::SecretsFile::serverKey( std::string_view type , std::string_view id_decoded )
276{
277 return serverKey( G::sv_to_string(type) , G::sv_to_string(id_decoded) ) ;
278}
279
280std::string GAuth::SecretsFile::serverKey( const std::string & type , const std::string & id_decoded )
281{
282 return std::string("server ",7U).append(G::Str::lower(type)).append(1U,' ').append(id_decoded) ;
283}
284
285std::string GAuth::SecretsFile::clientKey( std::string_view type , std::string_view selector )
286{
287 return std::string("client ",7U).append(G::Str::lower(type)).append(selector.empty()?0U:1U,' ').append(selector.data(),selector.size()) ;
288}
289
290bool GAuth::SecretsFile::containsClientSelector( std::string_view selector ) const
291{
292 return containsClientSecretImp( selector , false ) ;
293}
294
295bool GAuth::SecretsFile::containsClientSecret( std::string_view selector ) const
296{
297 return containsClientSecretImp( selector , true ) ;
298}
299
300bool GAuth::SecretsFile::containsClientSecretImp( std::string_view selector , bool with_id ) const
301{
302 if( !m_valid )
303 return false ;
304
305 reread() ;
306
307 auto p = m_contents.m_selectors.find( G::sv_to_string(selector) ) ;
308 auto end = m_contents.m_selectors.end() ;
309 if( with_id )
310 return p != end && (*p).second != 0U ;
311 else
312 return p != end ;
313}
314
315GAuth::Secret GAuth::SecretsFile::clientSecret( std::string_view type , std::string_view selector ) const
316{
317 if( !m_valid )
318 return Secret::none() ;
319
320 reread() ;
321
322 auto p = m_contents.m_map.find( clientKey(type,selector) ) ;
323 if( p == m_contents.m_map.end() )
324 return Secret::none() ;
325 else
326 return (*p).second ;
327}
328
329bool GAuth::SecretsFile::containsServerSecret( std::string_view type , std::string_view id_decoded ) const
330{
331 if( !m_valid )
332 return false ;
333
334 reread() ;
335
336 return id_decoded.empty() ?
337 m_contents.m_server_types.find( G::Str::lower(type) ) != m_contents.m_server_types.end() :
338 m_contents.m_map.find( serverKey(type,id_decoded) ) != m_contents.m_map.end() ;
339}
340
341GAuth::Secret GAuth::SecretsFile::serverSecret( std::string_view type , std::string_view id ) const
342{
343 if( !m_valid || id.empty() )
344 return Secret::none() ;
345
346 reread() ;
347
348 auto p = m_contents.m_map.find( serverKey(type,id) ) ;
349 if( p == m_contents.m_map.end() )
350 return Secret::none() ;
351 else
352 return (*p).second ;
353}
354
355std::pair<std::string,std::string> GAuth::SecretsFile::serverTrust( const std::string & address_range ) const
356{
357 std::pair<std::string,std::string> result ;
358 if( !m_valid )
359 return result ;
360
361 reread() ;
362
363 auto p = m_contents.m_trust_map.find( address_range ) ;
364 if( p != m_contents.m_trust_map.end() )
365 {
366 result.first = (*p).second.first ;
367 result.second = lineContext( (*p).second.second ) ;
368 }
369 return result ;
370}
371
372std::string GAuth::SecretsFile::path() const
373{
374 return m_path.str() ;
375}
376
377std::string GAuth::SecretsFile::lineContext( unsigned int line_number )
378{
379 return std::string("line ",5U).append(G::Str::fromUInt(line_number)) ;
380}
381
Encapsulates a userid/shared-secret/hash-function tuple from the secrets file.
Definition: gsecret.h:44
static Secret none()
Factory function that returns a secret that is not valid().
Definition: gsecret.cpp:75
static bool isDotted(std::string_view)
Returns true if the given secret string looks like it is in the old dotted format rather than base64.
Definition: gsecret.cpp:124
static std::string decode(Value)
Decodes a value.
Definition: gsecret.cpp:187
A class to read authentication secrets from file, used by GAuth::Secrets.
Definition: gsecretsfile.h:48
Secret serverSecret(std::string_view type, std::string_view id) const
Returns the server secret for the given id and type.
bool containsClientSecret(std::string_view selector) const
Returns true if a client secret is available with the given account selector.
static void check(const std::string &path, bool with_warnings)
Checks the given file.
bool containsServerSecret(std::string_view type, std::string_view id={}) const
Returns true if a server secret of the given type is available for the particular user or for any use...
Secret clientSecret(std::string_view type, std::string_view selector={}) const
Returns the client id and secret for the given type.
bool valid() const
Returns true if the file path was supplied in the ctor.
std::string path() const
Returns the file path, as supplied to the ctor.
bool containsClientSelector(std::string_view selector) const
Returns true if the given client account selector is valid.
SecretsFile(const G::Path &path, bool auto_reread, const std::string &debug_name)
Constructor to read "client" and "server" records from the named file.
std::pair< std::string, std::string > serverTrust(const std::string &address_range) const
Returns a non-empty trustee name if the server trusts remote clients in the given address range,...
static SystemTime time(const Path &file)
Returns the file's timestamp. Throws on error.
Definition: gfile.cpp:205
A Path object represents a file system path.
Definition: gpath.h:82
std::string basename() const
Returns the rightmost part of the path, ignoring "." parts.
Definition: gpath.cpp:339
std::string str() const
Returns the path string.
Definition: gpath.h:243
const iopath_char_type * iopath() const
Returns the path's string with a type that is suitable for initialising std::fstreams.
Definition: gpath.h:255
A class which acquires the process's special privileges on construction and releases them on destruct...
Definition: groot.h:52
static std::string lower(std::string_view)
Returns a copy of 's' in which all seven-bit upper-case characters have been replaced by lower-case c...
Definition: gstr.cpp:824
static bool imatch(char, char) noexcept
Returns true if the two characters are the same, ignoring seven-bit case.
Definition: gstr.cpp:1415
static std::istream & readLine(std::istream &stream, std::string &result, std::string_view eol={}, bool pre_erase_result=true, std::size_t limit=0U)
Reads a line from the stream using the given line terminator, which may be multi-character.
Definition: gstr.cpp:958
static std::string & trim(std::string &s, std::string_view ws)
Trims both ends of s, taking off any of the 'ws' characters.
Definition: gstr.cpp:338
static std::string fromUInt(unsigned int ui)
Converts unsigned int 'ui' to a string.
Definition: gstr.h:612
static std::string printable(const std::string &in, char escape='\\')
Returns a printable representation of the given input string, using chacter code ranges 0x20 to 0x7e ...
Definition: gstr.cpp:913
static std::string_view tailView(std::string_view in, std::size_t pos, std::string_view default_={}) noexcept
Like tail() but returning a view into the input string.
Definition: gstr.cpp:1337
static std::string_view ws() noexcept
Returns a string of standard whitespace characters.
Definition: gstr.cpp:1265
static std::string_view headView(std::string_view in, std::size_t pos, std::string_view default_={}) noexcept
Like head() but returning a view into the input string.
Definition: gstr.cpp:1308
A zero-copy string token iterator where the token separators are runs of whitespace characters,...
Definition: gstringtoken.h:54
Represents a unix-epoch time with microsecond resolution.
Definition: gdatetime.h:140
static SystemTime now()
Factory function for the current time.
Definition: gdatetime.cpp:328
bool sameSecond(const SystemTime &other) const noexcept
Returns true if this time and the other time are the same, at second resolution.
Definition: gdatetime.cpp:351
Low-level classes.
Definition: garg.h:36