E-MailRelay
gsecretsfile.cpp
Go to the documentation of this file.
1//
2// Copyright (C) 2001-2023 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 <fstream>
33#include <sstream>
34
35GAuth::SecretsFile::SecretsFile( const G::Path & path , bool auto_reread , const std::string & debug_name ) :
36 m_path(path) ,
37 m_auto(auto_reread) ,
38 m_debug_name(debug_name) ,
39 m_file_time(0) ,
40 m_check_time(G::SystemTime::now())
41{
42 m_valid = !path.str().empty() ;
43 if( m_valid )
44 read( path ) ;
45}
46
47void GAuth::SecretsFile::check( const std::string & path , bool with_warnings )
48{
49 if( !path.empty() )
50 {
51 Contents contents = readContents( path ) ;
52 showDiagnostics( contents , path , {} , with_warnings ) ;
53 if( contents.m_errors != 0U )
54 throw Error() ;
55 }
56}
57
59{
60 return m_valid ;
61}
62
63void GAuth::SecretsFile::reread() const
64{
65 (const_cast<SecretsFile*>(this))->reread(0) ;
66}
67
68void GAuth::SecretsFile::reread( int )
69{
70 if( m_auto )
71 {
73 G_DEBUG( "GAuth::SecretsFile::reread: file time checked at " << m_check_time << ": now " << now ) ;
74 if( !now.sameSecond(m_check_time) ) // at most once a second
75 {
76 m_check_time = now ;
77 G::SystemTime t = readFileTime( m_path ) ;
78 G_DEBUG( "GAuth::SecretsFile::reread: current file time " << t << ": saved file time " << m_file_time ) ;
79 if( t != m_file_time )
80 {
81 G_LOG_S( "GAuth::Secrets: re-reading secrets file: " << m_path ) ;
82 read( m_path ) ;
83 }
84 }
85 }
86}
87
88void GAuth::SecretsFile::read( const G::Path & path )
89{
90 m_file_time = readFileTime( path ) ;
91 m_contents = readContents( path ) ;
92 showDiagnostics( m_contents , path , m_debug_name , false ) ;
93}
94
95G::SystemTime GAuth::SecretsFile::readFileTime( const G::Path & path )
96{
97 G::Root claim_root ;
98 return G::File::time( path ) ;
99}
100
101GAuth::SecretsFile::Contents GAuth::SecretsFile::readContents( const G::Path & path )
102{
103 std::unique_ptr<std::ifstream> file ;
104 {
105 G::Root claim_root ;
106 file = std::make_unique<std::ifstream>( path.cstr() ) ;
107 }
108 if( !file->good() )
109 {
110 throw OpenError( path.str() ) ;
111 }
112
113 return readContents( *file ) ;
114}
115
116GAuth::SecretsFile::Contents GAuth::SecretsFile::readContents( std::istream & file )
117{
118 Contents contents ;
119 std::string line ;
120 for( unsigned int line_number = 1U ; file.good() ; ++line_number )
121 {
122 G::Str::readLine( file , line ) ;
123 if( !file )
124 break ;
125
126 G::Str::trim( line , G::Str::ws() ) ;
127 if( !line.empty() && line.at(0U) != '#' )
128 {
129 G::string_view line_sv( line ) ;
130 G::StringTokenView t( line_sv , " \t"_sv ) ;
131 G::string_view w1 = t() ;
132 G::string_view w2 = (++t)() ;
133 G::string_view w3 = (++t)() ;
134 G::string_view w4 = (++t)() ;
135 bool sufficient = t.valid() ;
136 G::string_view w5 = (++t)() ;
137 bool excess = (++t).valid() ;
138 if( !w5.empty() && w5.at(0) == '#' )
139 w5 = G::string_view() , excess = false ;
140
141 if( excess )
142 addWarning( contents , line_number , "too many fields"_sv ) ;
143
144 if( sufficient )
145 processLine( contents , line_number , w1 , w2 , w3 , w4 , w5 ) ;
146 else
147 addError( contents , line_number , "too few fields"_sv ) ;
148 }
149 }
150 return contents ;
151}
152
153void GAuth::SecretsFile::processLine( Contents & contents , unsigned int line_number ,
154 G::string_view side , G::string_view type_in , G::string_view id ,
155 G::string_view secret , G::string_view selector )
156{
157 G::string_view type = canonicalView( G::Str::headView( type_in , ":" , false ) ) ;
158 G::string_view type_decoration = G::Str::tailView( type_in , ":" ) ;
159 bool is_server_side = G::Str::imatch( side , "server"_sv ) ;
160 bool is_client_side = G::Str::imatch( side , "client"_sv ) ;
161
162 if( is_server_side && G::Str::imatch( type , "none"_sv ) )
163 {
164 G::string_view ip_range = id ;
165 G::string_view keyword = secret ;
166 bool inserted = contents.m_trust_map.insert( {G::sv_to_string(ip_range),{G::sv_to_string(keyword),line_number}} ).second ;
167 if( !inserted )
168 addError( contents , line_number , "duplicate server trust address"_sv ) ;
169 }
170 else if( is_client_side && G::Str::imatch(type_in,"plain:b") && id == "="_sv && secret == "="_sv )
171 {
172 contents.m_selectors.insert( {G::sv_to_string(selector),0U} ) ;
173 }
174 else
175 {
176 G::string_view id_encoding ;
177 G::string_view secret_encoding ;
178 G::string_view hash_function ;
179 if( G::Str::imatch( type.substr(0U,5U) , "plain" ) )
180 {
181 id_encoding = G::Str::imatch( type_decoration , "b" ) ? "base64"_sv : "xtext"_sv ; // should also allow plain:xb etc
182 secret_encoding = id_encoding ;
183 //hash_function = "" ;
184 }
185 else if( G::Str::imatch( type , "md5"_sv ) && Secret::isDotted(secret) )
186 {
187 id_encoding = "xtext"_sv ;
188 secret_encoding = "dotted"_sv ;
189 hash_function = "md5"_sv ;
190 }
191 else
192 {
193 id_encoding = "xtext"_sv ;
194 secret_encoding = "base64"_sv ;
195 hash_function = type ;
196 }
197
198 if( is_server_side )
199 {
200 std::string key = serverKey( type , Secret::decode({id,id_encoding}) ) ;
201 Secret secret_obj( {id,id_encoding} , {secret,secret_encoding} , hash_function , lineContext(line_number) ) ;
202 bool inserted = contents.m_map.insert( {key,secret_obj} ).second ;
203 if( inserted )
204 contents.m_types.insert( G::Str::lower(type) ) ;
205 else
206 addError( contents , line_number , "duplicate server secret"_sv ) ;
207 }
208 else if( is_client_side )
209 {
210 std::string key = clientKey( type , selector ) ;
211 Secret secret_obj( {id,id_encoding} , {secret,secret_encoding} , hash_function , lineContext(line_number) ) ;
212 bool inserted = contents.m_map.insert( {key,secret_obj} ).second ;
213 if( inserted )
214 ((*(contents.m_selectors.insert( {G::sv_to_string(selector),0U} ).first)).second)++ ;
215 else
216 addError( contents , line_number , "duplicate client secret"_sv ) ;
217 }
218 else
219 {
220 addError( contents , line_number , "invalid value in first field"_sv , side ) ;
221 }
222 }
223}
224
225void GAuth::SecretsFile::addWarning( Contents & contents , unsigned int line_number , G::string_view message , G::string_view more )
226{
227 contents.m_diagnostics.emplace_back( false , line_number , join(message,more) ) ;
228}
229
230void GAuth::SecretsFile::addError( Contents & contents , unsigned int line_number , G::string_view message , G::string_view more )
231{
232 contents.m_diagnostics.emplace_back( true , line_number , join(message,more) ) ;
233 contents.m_errors++ ;
234}
235
236std::string GAuth::SecretsFile::join( G::string_view message_in , G::string_view more )
237{
238 std::string message( message_in.data() , message_in.size() ) ;
239 if( !more.empty() )
240 message.append(": [",3U).append(G::Str::printable(more)).append(1U,']') ;
241 return message ;
242}
243
244void GAuth::SecretsFile::showDiagnostics( const Contents & c , const G::Path & path , const std::string & debug_name , bool with_warnings )
245{
246 if( c.m_diagnostics.empty() )
247 return ;
248
249 if( !with_warnings && c.m_errors == 0U )
250 return ;
251
252 G_WARNING( "GAuth::SecretsFile::read: problems reading" << (debug_name.empty()?"":" ") << debug_name << " "
253 "secrets file [" << path.str() << "]..." ) ;
254
255 std::string prefix = path.basename() ;
256 for( const auto & d : c.m_diagnostics )
257 {
258 if( std::get<0>(d) )
259 G_ERROR( "GAuth::SecretsFile::read: " << prefix << "(" << std::get<1>(d) << "): " << std::get<2>(d) ) ;
260 else if( with_warnings )
261 G_WARNING( "GAuth::SecretsFile::read: " << prefix << "(" << std::get<1>(d) << "): " << std::get<2>(d) ) ;
262 }
263}
264
265G::string_view GAuth::SecretsFile::canonicalView( G::string_view type )
266{
267 // (for backwards compatibility -- new code exects plain, md5, sha1, sha512 etc)
268 if( G::Str::imatch( type , "cram-md5"_sv ) ) return "md5" ;
269 if( G::Str::imatch( type , "apop"_sv ) ) return "md5" ;
270 if( G::Str::imatch( type , "login"_sv ) ) return "plain" ;
271 return type ;
272}
273
274std::string GAuth::SecretsFile::serverKey( G::string_view type , G::string_view id_decoded )
275{
276 return serverKey( G::sv_to_string(type) , G::sv_to_string(id_decoded) ) ;
277}
278
279std::string GAuth::SecretsFile::serverKey( const std::string & type , const std::string & id_decoded )
280{
281 return std::string("server ",7U).append(G::Str::lower(type)).append(1U,' ').append(id_decoded) ;
282}
283
284std::string GAuth::SecretsFile::clientKey( G::string_view type , G::string_view selector )
285{
286 return std::string("client ",7U).append(G::Str::lower(type)).append(selector.empty()?0U:1U,' ').append(selector.data(),selector.size()) ;
287}
288
290{
291 return containsClientSecretImp( selector , false ) ;
292}
293
295{
296 return containsClientSecretImp( selector , true ) ;
297}
298
299bool GAuth::SecretsFile::containsClientSecretImp( G::string_view selector , bool with_id ) const
300{
301 if( !m_valid )
302 return false ;
303
304 reread() ;
305
306 auto p = m_contents.m_selectors.find( G::sv_to_string(selector) ) ;
307 auto end = m_contents.m_selectors.end() ;
308 if( with_id )
309 return p != end && (*p).second != 0U ;
310 else
311 return p != end ;
312}
313
315{
316 if( !m_valid )
317 return Secret::none() ;
318
319 reread() ;
320
321 auto p = m_contents.m_map.find( clientKey(type,selector) ) ;
322 if( p == m_contents.m_map.end() )
323 return Secret::none() ;
324 else
325 return (*p).second ;
326}
327
329{
330 if( !m_valid )
331 return false ;
332
333 reread() ;
334
335 return id_decoded.empty() ?
336 m_contents.m_types.find( G::Str::lower(type) ) != m_contents.m_types.end() :
337 m_contents.m_map.find( serverKey(type,id_decoded) ) != m_contents.m_map.end() ;
338}
339
341{
342 if( !m_valid || id.empty() )
343 return Secret::none() ;
344
345 reread() ;
346
347 auto p = m_contents.m_map.find( serverKey(type,id) ) ;
348 if( p == m_contents.m_map.end() )
349 return Secret::none() ;
350 else
351 return (*p).second ;
352}
353
354std::pair<std::string,std::string> GAuth::SecretsFile::serverTrust( const std::string & address_range ) const
355{
356 std::pair<std::string,std::string> result ;
357 if( !m_valid )
358 return result ;
359
360 reread() ;
361
362 auto p = m_contents.m_trust_map.find( address_range ) ;
363 if( p != m_contents.m_trust_map.end() )
364 {
365 result.first = (*p).second.first ;
366 result.second = lineContext( (*p).second.second ) ;
367 }
368 return result ;
369}
370
371std::string GAuth::SecretsFile::path() const
372{
373 return m_path.str() ;
374}
375
376std::string GAuth::SecretsFile::lineContext( unsigned int line_number )
377{
378 return std::string("line ",5U).append(G::Str::fromUInt(line_number)) ;
379}
380
Encapsulates a userid/shared-secret/hash-function tuple from the secrets file.
Definition: gsecret.h:43
static Secret none()
Factory function that returns a secret that is not valid().
Definition: gsecret.cpp:75
static bool isDotted(G::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 clientSecret(G::string_view type, G::string_view selector={}) const
Returns the client id and secret for the given type.
bool containsClientSelector(G::string_view selector) const
Returns true if the given client account selector is valid.
bool containsServerSecret(G::string_view type, G::string_view id={}) const
Returns true if a server secret of the given type is available for the particular user or for any use...
static void check(const std::string &path, bool with_warnings)
Checks the given file.
bool valid() const
Returns true if the file path was supplied in the ctor.
Secret serverSecret(G::string_view type, G::string_view id) const
Returns the server secret for the given id and type.
std::string path() const
Returns the file path, as supplied to the ctor.
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,...
bool containsClientSecret(G::string_view selector) const
Returns true if a client secret is available with the given account selector.
static SystemTime time(const Path &file)
Returns the file's timestamp. Throws on error.
Definition: gfile.cpp:215
A Path object represents a file system path.
Definition: gpath.h:73
const char * cstr() const noexcept
Returns the path string.
Definition: gpath.h:230
std::string basename() const
Returns the rightmost part of the path, ignoring "." parts.
Definition: gpath.cpp:346
std::string str() const
Returns the path string.
Definition: gpath.h:224
A class which acquires the process's special privileges on construction and releases them on destruct...
Definition: groot.h:52
static string_view headView(string_view in, std::size_t pos, string_view default_={}) noexcept
Like head() but returning a view into the input string.
Definition: gstr.cpp:1311
static string_view tailView(string_view in, std::size_t pos, string_view default_={}) noexcept
Like tail() but returning a view into the input string.
Definition: gstr.cpp:1340
static std::istream & readLine(std::istream &stream, std::string &result, 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:961
static bool imatch(char, char) noexcept
Returns true if the two characters are the same, ignoring seven-bit case.
Definition: gstr.cpp:1418
static std::string lower(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:827
static std::string fromUInt(unsigned int ui)
Converts unsigned int 'ui' to a string.
Definition: gstr.h:616
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:916
static string_view ws() noexcept
Returns a string of standard whitespace characters.
Definition: gstr.cpp:1268
static std::string & trim(std::string &s, string_view ws)
Trims both ends of s, taking off any of the 'ws' characters.
Definition: gstr.cpp:338
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:134
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
A class like c++17's std::string_view.
Definition: gstringview.h:51
Low-level classes.
Definition: garg.h:30