E-MailRelay

gsecretsfile.cpp
Go to the documentation of this file.
1//
2// Copyright (C) 2001-2021 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 "gdatetime.h"
29#include "gfile.h"
30#include "gassert.h"
31#include <map>
32#include <set>
33#include <fstream>
34#include <string>
35#include <algorithm> // std::swap()
36#include <utility> // std::swap(), std::pair
37#include <sstream>
38
39 GAuth::SecretsFile::SecretsFile( const G::Path & path , bool auto_reread , const std::string & debug_name ) :
40 m_path(path) ,
41 m_auto(auto_reread) ,
42 m_debug_name(debug_name) ,
43 m_file_time(0) ,
44 m_check_time(G::SystemTime::now())
45{
46 m_valid = ! path.str().empty() ;
47 if( m_valid )
48 read( path ) ;
49}
50
51 void GAuth::SecretsFile::check( const std::string & path )
52{
53 if( !path.empty() )
54 {
55 Contents contents = readContents( path ) ;
56 showWarnings( contents.m_warnings , path ) ;
57 if( !contents.m_warnings.empty() )
58 throw Error() ;
59 }
60}
61
62 bool GAuth::SecretsFile::valid() const
63{
64 return m_valid ;
65}
66
67 void GAuth::SecretsFile::reread() const
68{
69 (const_cast<SecretsFile*>(this))->reread(0) ;
70}
71
72 void GAuth::SecretsFile::reread( int )
73{
74 if( m_auto )
75 {
76 G::SystemTime now = G::SystemTime::now() ;
77 G_DEBUG( "GAuth::SecretsFile::reread: file time checked at " << m_check_time << ": now " << now ) ;
78 if( !now.sameSecond(m_check_time) ) // at most once a second
79 {
80 m_check_time = now ;
81 G::SystemTime t = readFileTime( m_path ) ;
82 G_DEBUG( "GAuth::SecretsFile::reread: current file time " << t << ": saved file time " << m_file_time ) ;
83 if( t != m_file_time )
84 {
85 G_LOG_S( "GAuth::Secrets: re-reading secrets file: " << m_path ) ;
86 read( m_path ) ;
87 }
88 }
89 }
90}
91
92 void GAuth::SecretsFile::read( const G::Path & path )
93{
94 m_file_time = readFileTime( path ) ;
95 m_contents = readContents( path ) ;
96 showWarnings( m_contents.m_warnings , path , m_debug_name ) ;
97}
98
99 G::SystemTime GAuth::SecretsFile::readFileTime( const G::Path & path )
100{
101 G::Root claim_root ;
102 return G::File::time( path ) ;
103}
104
105GAuth::SecretsFile::Contents GAuth::SecretsFile::readContents( const G::Path & path )
106{
107 std::unique_ptr<std::ifstream> file ;
108 {
109 G::Root claim_root ;
110 file = std::make_unique<std::ifstream>( path.cstr() ) ;
111 }
112 if( !file->good() )
113 {
114 throw Secrets::OpenError( path.str() ) ;
115 }
116
117 return readContents( *file ) ;
118}
119
120GAuth::SecretsFile::Contents GAuth::SecretsFile::readContents( std::istream & file )
121{
122 Contents contents ;
123 for( unsigned int line_number = 1U ; file.good() ; ++line_number )
124 {
125 std::string line = G::Str::readLineFrom( file ) ;
126 if( !file )
127 break ;
128
129 G::Str::trim( line , G::Str::ws() ) ;
130 if( !line.empty() && line.at(0U) != '#' )
131 {
132 G::StringArray word_array ;
133 G::Str::splitIntoTokens( line , word_array , " \t" ) ;
134 if( word_array.size() >= 4U )
135 {
136 // 0=<client-server> 1=<encoding-type> 2=<userid-or-ipaddress> 3=<secret-or-verifier-hint>
137 processLine( contents , line_number , word_array[0U] , word_array[1U] , word_array[2U] , word_array[3U] ) ;
138 }
139 else
140 {
141 addWarning( contents , line_number , "too few fields" ) ;
142 }
143 }
144 }
145 return contents ;
146}
147
148 void GAuth::SecretsFile::processLine( Contents & contents ,
149 unsigned int line_number , const std::string & side , const std::string & type_in ,
150 const std::string & id_or_ip_in , const std::string & secret_in )
151{
152 std::string type = G::Str::lower( type_in ) ;
153 std::string id_or_ip = id_or_ip_in ;
154 std::string secret = secret_in ;
155 if( type == "plain:b" )
156 {
157 // for now just re-encode to xtext -- TODO rework secrets-file encodings
158 bool valid_id = G::Base64::valid( id_or_ip ) ;
159 bool valid_secret = G::Base64::valid( secret ) ;
160 if( !valid_id )
161 addWarning( contents , line_number , "invalid base64 encoding in third field" , id_or_ip ) ;
162 if( !valid_secret )
163 addWarning( contents , line_number , "invalid base64 encoding in fourth field" ) ;
164 if( !valid_id || !valid_secret )
165 return ;
166 type = "plain" ;
167 id_or_ip = G::Xtext::encode( G::Base64::decode(id_or_ip) ) ;
168 secret = G::Xtext::encode( G::Base64::decode(secret) ) ;
169 }
170 processLineImp( contents , line_number , G::Str::lower(side) , type , id_or_ip , secret ) ;
171}
172
173 void GAuth::SecretsFile::processLineImp( Contents & contents ,
174 unsigned int line_number , const std::string & side , const std::string & type ,
175 const std::string & id_or_ip , const std::string & secret )
176{
177 G_ASSERT( !side.empty() && !type.empty() && !id_or_ip.empty() && !secret.empty() ) ;
178
179 if( type == "plain" )
180 {
181 if( !G::Xtext::valid(id_or_ip) ) // (ip address ranges are valid xtext)
182 addWarning( contents , line_number , "invalid xtext encoding in third field" , id_or_ip ) ;
183 if( !G::Xtext::valid(secret) )
184 addWarning( contents , line_number , "invalid xtext encoding in fourth field" ) ; // (new)
185 }
186
187 if( side == "server" )
188 {
189 // server-side
190 std::string key = serverKey( type , id_or_ip ) ;
191 Value value( secret , line_number ) ;
192 bool inserted = contents.m_map.insert(std::make_pair(key,value)).second ;
193 if( inserted )
194 contents.m_types.insert( canonical(type) ) ;
195 else
196 addWarning( contents , line_number , "duplicate server secret" , key ) ;
197 }
198 else if( side == "client" )
199 {
200 // client-side
201 const std::string & id = id_or_ip ;
202 std::string key = clientKey( type ) ; // not including user id
203 Value value( id + " " + secret , line_number ) ;
204 bool inserted = contents.m_map.insert(std::make_pair(key,value)).second ;
205 if( !inserted )
206 addWarning( contents , line_number , "too many client secrets" , key ) ;
207 }
208 else
209 {
210 addWarning( contents , line_number , "invalid value in first field" , side ) ;
211 }
212}
213
214 void GAuth::SecretsFile::addWarning( Contents & contents , unsigned int line_number , const std::string & message_in , const std::string & more )
215{
216 std::string message = more.empty() ? message_in : ( message_in + ": [" + G::Str::printable(more) + "]" ) ;
217 contents.m_warnings.push_back( Warnings::value_type(line_number,message) ) ;
218}
219
220 void GAuth::SecretsFile::showWarnings( const Warnings & warnings , const G::Path & path , const std::string & debug_name )
221{
222 if( !warnings.empty() )
223 {
224 std::string prefix = path.basename() ;
225 G_WARNING( "GAuth::SecretsFile::read: problems reading" << (debug_name.empty()?"":" ") << debug_name << " secrets file [" << path.str() << "]..." ) ;
226 for( const auto & warning : warnings )
227 {
228 G_WARNING( "GAuth::SecretsFile::read: " << prefix << "(" << warning.first << "): " << warning.second ) ;
229 }
230 }
231}
232
233std::string GAuth::SecretsFile::canonical( const std::string & type_in )
234{
235 // (cram-md5, apop and login are for backwards compatibility -- new
236 // code exects plain, md5, sha1, sha512 etc)
237 std::string type = G::Str::lower( type_in ) ;
238 if( type == "cram-md5" ) type = "md5" ;
239 if( type == "apop" ) type = "md5" ;
240 if( type == "login" ) type = "plain" ;
241 return type ;
242}
243
244std::string GAuth::SecretsFile::serverKey( const std::string & type , const std::string & id_xtext )
245{
246 // eg. key -> value...
247 // "server plain bob" -> "e+3Dmc2"
248 // "server md5 bob" -> "xbase64x=="
249 // "server none 192.168.0.0/24" -> "trustee"
250 // "server none ::1/128" -> "trustee"
251 return "server " + canonical(type) + " " + id_xtext ;
252}
253
254std::string GAuth::SecretsFile::clientKey( const std::string & type )
255{
256 // eg. key -> value...
257 // "client plain" -> "alice secret+21"
258 // "client md5" -> "alice xbase64x=="
259 return "client " + canonical(type) ;
260}
261
262 GAuth::Secret GAuth::SecretsFile::clientSecret( const std::string & type ) const
263{
264 reread() ;
265
266 auto p = m_contents.m_map.find( clientKey(type) ) ;
267 if( p == m_contents.m_map.end() )
268 {
269 return Secret::none() ;
270 }
271 else
272 {
273 std::string id_xtext = G::Str::head( (*p).second.s , " " ) ;
274 std::string secret_encoded = G::Str::tail( (*p).second.s , " " ) ;
275 return Secret( secret_encoded , canonical(type) , id_xtext , true , line((*p).second.n) ) ;
276 }
277}
278
279 GAuth::Secret GAuth::SecretsFile::serverSecret( const std::string & type , const std::string & id ) const
280{
281 if( id.empty() )
282 return Secret::none() ;
283
284 reread() ;
285
286 auto p = m_contents.m_map.find( serverKey(type,G::Xtext::encode(id)) ) ;
287 if( p == m_contents.m_map.end() )
288 {
289 return Secret::none( id ) ;
290 }
291 else
292 {
293 return Secret( (*p).second.s , canonical(type) , id , false , line((*p).second.n) ) ;
294 }
295}
296
297std::pair<std::string,std::string> GAuth::SecretsFile::serverTrust( const std::string & address_range ) const
298{
299 reread() ; // (new)
300
301 std::pair<std::string,std::string> result ;
302 std::string type = "none" ;
303 const std::string & id = address_range ; // the address-range lives in the id field
304 auto p = m_contents.m_map.find( serverKey(type,G::Xtext::encode(id)) ) ;
305 if( p != m_contents.m_map.end() )
306 {
307 result.first = (*p).second.s ; // the trustee name lives in the shared-secret field
308 result.second = line( (*p).second.n ) ;
309 }
310 return result ;
311}
312
313std::string GAuth::SecretsFile::path() const
314{
315 return m_path.str() ;
316}
317
318 bool GAuth::SecretsFile::contains( const std::string & type , const std::string & id ) const
319{
320 return id.empty() ?
321 m_contents.m_types.find( canonical(type) ) != m_contents.m_types.end() :
322 m_contents.m_map.find( serverKey(type,G::Xtext::encode(id)) ) != m_contents.m_map.end() ;
323}
324
325std::string GAuth::SecretsFile::line( unsigned int line_number )
326{
327 return "line " + G::Str::fromUInt(line_number) ;
328}
GAuth::Secret
Encapsulates a shared secret from the secrets file plus the associated userid.
Definition: gsecret.h:42
GAuth::Secret::none
static Secret none()
Factory function that returns a secret that is not valid() and has an empty id().
Definition: gsecret.cpp:94
GAuth::SecretsFile
A class to read authentication secrets from file, used by GAuth::Secrets.
Definition: gsecretsfile.h:46
GAuth::SecretsFile::serverSecret
Secret serverSecret(const std::string &type, const std::string &id) const
Returns the server secret for the given id and type.
Definition: gsecretsfile.cpp:279
GAuth::SecretsFile::check
static void check(const std::string &path)
Checks the given file.
Definition: gsecretsfile.cpp:51
GAuth::SecretsFile::valid
bool valid() const
Returns true if the file path was supplied in the ctor.
Definition: gsecretsfile.cpp:62
GAuth::SecretsFile::path
std::string path() const
Returns the file path, as supplied to the ctor.
Definition: gsecretsfile.cpp:313
GAuth::SecretsFile::contains
bool contains(const std::string &type, const std::string &id={}) const
Returns true if a server secret of the given type is available for the particular user or any user.
Definition: gsecretsfile.cpp:318
GAuth::SecretsFile::SecretsFile
SecretsFile(const G::Path &path, bool auto_reread, const std::string &debug_name)
Constructor to read "client" and "server" records from the named file.
Definition: gsecretsfile.cpp:39
GAuth::SecretsFile::serverTrust
std::pair< std::string, std::string > serverTrust(const std::string &address_range) const
Returns a non-empty trustee name if the server trusts clients in the given address range,...
Definition: gsecretsfile.cpp:297
GAuth::SecretsFile::clientSecret
Secret clientSecret(const std::string &type) const
Returns the client id and secret for the given type.
Definition: gsecretsfile.cpp:262
G::Base64::decode
static std::string decode(const std::string &, bool throw_on_invalid=false, bool strict=true)
Decodes the given string.
Definition: gbase64.cpp:89
G::Base64::valid
static bool valid(const std::string &, bool strict=true)
Returns true if the string is a valid base64 encoding, possibly allowing for embedded newlines,...
Definition: gbase64.cpp:94
G::File::time
static SystemTime time(const Path &file)
Returns the file's timestamp. Throws on error.
Definition: gfile.cpp:211
G::Path
A Path object represents a file system path.
Definition: gpath.h:72
G::Path::cstr
const char * cstr() const noexcept
Returns the path string.
Definition: gpath.h:221
G::Path::basename
std::string basename() const
Returns the rightmost part of the path, ignoring "." parts.
Definition: gpath.cpp:328
G::Path::str
std::string str() const
Returns the path string.
Definition: gpath.h:215
G::Root
A class which acquires the process's special privileges on construction and releases them on destruct...
Definition: groot.h:52
G::Str::ws
static string_view ws()
Returns a string of standard whitespace characters.
Definition: gstr.cpp:1255
G::Str::tail
static std::string tail(const std::string &in, std::size_t pos, const std::string &default_=std::string())
Returns the last part of the string after the given position.
Definition: gstr.cpp:1287
G::Str::splitIntoTokens
static void splitIntoTokens(const std::string &in, StringArray &out, string_view ws, char esc='\0')
Splits the string into 'ws'-delimited tokens.
Definition: gstr.cpp:1073
G::Str::fromUInt
static std::string fromUInt(unsigned int ui)
Converts unsigned int 'ui' to a string.
Definition: gstr.h:579
G::Str::printable
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:885
G::Str::head
static std::string head(const std::string &in, std::size_t pos, const std::string &default_=std::string())
Returns the first part of the string up to just before the given position.
Definition: gstr.cpp:1273
G::Str::lower
static std::string lower(const std::string &s)
Returns a copy of 's' in which all Latin-1 upper-case characters have been replaced by lower-case cha...
Definition: gstr.cpp:741
G::Str::readLineFrom
static std::string readLineFrom(std::istream &stream, const std::string &eol=std::string())
Reads a line from the stream using the given line terminator.
Definition: gstr.cpp:924
G::Str::trim
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:359
G::SystemTime
Represents a unix-epoch time with microsecond resolution.
Definition: gdatetime.h:125
G::SystemTime::now
static SystemTime now()
Factory function for the current time.
Definition: gdatetime.cpp:260
G::SystemTime::sameSecond
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:281
G::Xtext::valid
static bool valid(const std::string &, bool strict=false)
Returns true if a valid encoding.
Definition: gxtext.cpp:75
G::Xtext::encode
static std::string encode(const std::string &)
Encodes the given string.
Definition: gxtext.cpp:95
gassert.h
gbase64.h
gdatetime.h
gfile.h
groot.h
gsecrets.h
gsecretsfile.h
gstr.h
gxtext.h
G
Low-level classes.
Definition: galign.h:28
G::StringArray
std::vector< std::string > StringArray
A std::vector of std::strings.
Definition: gstrings.h:31