KiCad PCB EDA Suite
Loading...
Searching...
No Matches
kigit_ssh_keys.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright The KiCad Developers, see AUTHORS.TXT for contributors.
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 3
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 */
19
20#include "kigit_ssh_keys.h"
21
22#include <algorithm>
23#include <fstream>
24#include <iterator>
25#include <string>
26
27#include <wx/arrstr.h>
28#include <wx/filename.h>
29#include <wx/utils.h>
30
31#ifndef _WIN32
32#include <cerrno>
33#include <cstdlib>
34#include <pwd.h>
35#include <unistd.h>
36#endif
37
38
39namespace
40{
41
42// Quotes only group, so a Windows path keeps its backslashes
43std::vector<wxString> splitArgs( const wxString& aText )
44{
45 std::vector<wxString> args;
46 wxString current;
47 bool quoted = false;
48 bool inArg = false;
49
50 for( wxUniChar c : aText )
51 {
52 // As ssh, a comment starts only where an argument could
53 if( !quoted && !inArg && c == '#' )
54 break;
55
56 if( c == '"' )
57 {
58 quoted = !quoted;
59 inArg = true;
60 }
61 else if( !quoted && ( c == ' ' || c == '\t' ) )
62 {
63 if( inArg )
64 args.push_back( current );
65
66 current.clear();
67 inArg = false;
68 }
69 else
70 {
71 current += c;
72 inArg = true;
73 }
74 }
75
76 if( inArg )
77 args.push_back( current );
78
79 return args;
80}
81
82
83// A keyword ends at whitespace or '=', and either or both may separate it from its arguments
84void splitKeyword( const wxString& aLine, wxString& aKeyword, wxString& aArgs )
85{
86 size_t end = aLine.find_first_of( wxS( " \t=" ) );
87
88 if( end == wxString::npos )
89 {
90 aKeyword = aLine;
91 aArgs.clear();
92 return;
93 }
94
95 aKeyword = aLine.substr( 0, end );
96
97 size_t start = aLine.find_first_not_of( wxS( " \t" ), end );
98
99 if( start != wxString::npos && aLine[start] == '=' )
100 start = aLine.find_first_not_of( wxS( " \t" ), start + 1 );
101
102 aArgs = start == wxString::npos ? wxString() : aLine.substr( start );
103}
104
105
106// A matching negated pattern rejects the host even when another pattern accepts it. ssh compares
107// patterns case-sensitively with the host as typed in the address
108bool hostMatches( const std::vector<wxString>& aPatterns, const wxString& aHost )
109{
110 bool matched = false;
111
112 for( const wxString& pattern : aPatterns )
113 {
114 const bool negated = pattern.StartsWith( wxS( "!" ) );
115 wxString glob = negated ? pattern.Mid( 1 ) : pattern;
116
117 if( !aHost.Matches( glob ) )
118 continue;
119
120 if( negated )
121 return false;
122
123 matched = true;
124 }
125
126 return matched;
127}
128
129
130wxString expandPath( const wxString& aValue, const wxString& aHomeDir )
131{
132 // The other tokens need the connection's user, port and the like, which we do not know
133 for( size_t pos = aValue.find( '%' ); pos != wxString::npos; pos = aValue.find( '%', pos + 2 ) )
134 {
135 if( pos + 1 >= aValue.size() || aValue[pos + 1] != 'd' )
136 return aValue;
137 }
138
139 wxString head;
140 wxString tail = aValue;
141
142 // "~user" names another account's home, which is not ours to guess
143 if( aValue == wxS( "~" ) || aValue.StartsWith( wxS( "~/" ) ) || aValue.StartsWith( wxS( "~\\" ) ) )
144 {
145 head = aHomeDir;
146 tail = aValue.Mid( 1 );
147 }
148
149 tail.Replace( wxS( "%d" ), aHomeDir );
150 return head + tail;
151}
152
153} // namespace
154
155
156std::vector<wxString> KIGIT::ParseSSHConfigIdentityFiles( const wxString& aConfigText, const wxString& aHost,
157 const wxString& aHomeDir )
158{
159 std::vector<wxString> files;
160
161 // Lines before the first Host apply to every host
162 bool active = true;
163
164 for( wxString line : wxSplit( aConfigText, '\n', '\0' ) )
165 {
166 line.Trim( true ).Trim( false );
167
168 if( line.IsEmpty() || line.StartsWith( wxS( "#" ) ) )
169 continue;
170
171 wxString keyword;
172 wxString args;
173 splitKeyword( line, keyword, args );
174 keyword.MakeLower();
175
176 if( keyword == wxS( "host" ) )
177 {
178 active = hostMatches( splitArgs( args ), aHost );
179 }
180 else if( keyword == wxS( "match" ) )
181 {
182 // Match criteria need the resolved connection, so its block is never applied
183 active = false;
184 }
185 else if( active && keyword == wxS( "identityfile" ) )
186 {
187 std::vector<wxString> values = splitArgs( args );
188
189 if( !values.empty() )
190 files.push_back( expandPath( values.front(), aHomeDir ) );
191 }
192 }
193
194 return files;
195}
196
197
198std::vector<wxString> KIGIT::DefaultSSHKeyCandidates( const wxString& aHomeDir, const wxString& aHost )
199{
200 std::vector<wxString> keys;
201
202 // An empty home would make every key path relative to the working directory
203 if( aHomeDir.IsEmpty() )
204 return keys;
205
206 wxFileName sshDir( aHomeDir, wxEmptyString );
207 sshDir.AppendDir( wxS( ".ssh" ) );
208
209 const wxString configPath = wxFileName( sshDir.GetPath(), wxS( "config" ) ).GetFullPath();
210 std::vector<wxString> candidates;
211 std::ifstream config( configPath.fn_str(), std::ios::binary );
212
213 if( config )
214 {
215 std::string text( ( std::istreambuf_iterator<char>( config ) ), std::istreambuf_iterator<char>() );
216
217 // ssh rejects a BOM, but Windows editors write one and it would hide the first Host line
218 if( text.rfind( "\xEF\xBB\xBF", 0 ) == 0 )
219 text.erase( 0, 3 );
220
221 wxString decoded = wxString::FromUTF8( text );
222
223 // An ANSI-saved config would otherwise decode to nothing
224 if( decoded.IsEmpty() && !text.empty() )
225 decoded = wxString( text.data(), wxConvISO8859_1, text.size() );
226
227 candidates = ParseSSHConfigIdentityFiles( decoded, aHost, aHomeDir );
228 }
229
230 for( const wxString& name : { wxS( "id_ed25519" ), wxS( "id_ecdsa" ), wxS( "id_rsa" ), wxS( "id_dsa" ) } )
231 candidates.push_back( wxFileName( sshDir.GetPath(), name ).GetFullPath() );
232
233 for( const wxString& candidate : candidates )
234 {
235 wxFileName fn( candidate );
236
237 // ssh would resolve a relative path against its working directory, which ours is not
238 if( !fn.IsAbsolute() || !IsOfferableSSHKey( fn.GetFullPath() ) )
239 continue;
240
241 const wxString path = fn.GetFullPath();
242
243 bool seen = std::any_of( keys.begin(), keys.end(),
244 [&]( const wxString& aKey )
245 {
246 return IsSameSSHKey( aKey, path );
247 } );
248
249 if( !seen )
250 keys.push_back( path );
251 }
252
253 return keys;
254}
255
256
257bool KIGIT::IsSameSSHKey( const wxString& aKey, const wxString& aOther )
258{
259 if( aKey.IsEmpty() || aOther.IsEmpty() )
260 return aKey == aOther;
261
262 return wxFileName( aKey ).GetFullPath().IsSameAs( wxFileName( aOther ).GetFullPath(),
263 wxFileName::IsCaseSensitive() );
264}
265
266
267// wxGetenv converts through an unlocked static buffer except on MSVC, and key discovery runs on any thread
269{
270#ifdef _WIN32
271 return wxGetHomeDir();
272#else
273 if( const char* home = std::getenv( "HOME" ); home && *home )
274 return wxString( home, wxConvLibc );
275
276 passwd entry;
277 passwd* found = nullptr;
278 std::vector<char> buffer( 1024 );
279 int rc;
280
281 while( ( rc = getpwuid_r( getuid(), &entry, buffer.data(), buffer.size(), &found ) ) == ERANGE )
282 buffer.resize( buffer.size() * 2 );
283
284 if( rc == 0 && found && found->pw_dir )
285 return wxString( found->pw_dir, wxConvLibc );
286
287 return wxEmptyString;
288#endif
289}
290
291
292bool KIGIT::IsOfferableSSHKey( const wxString& aKey )
293{
294 return !aKey.IsEmpty() && wxFileName::FileExists( aKey ) && wxFileName::FileExists( aKey + wxS( ".pub" ) );
295}
const char * name
APIEXPORT bool IsOfferableSSHKey(const wxString &aKey)
Whether aKey can be offered to a server.
APIEXPORT std::vector< wxString > ParseSSHConfigIdentityFiles(const wxString &aConfigText, const wxString &aHost, const wxString &aHomeDir)
The IdentityFile entries an ssh config gives aHost, in file order.
APIEXPORT wxString HomeDir()
The user's home folder, where ssh looks for its config and default keys.
APIEXPORT bool IsSameSSHKey(const wxString &aKey, const wxString &aOther)
Whether two key paths name the same file.
APIEXPORT std::vector< wxString > DefaultSSHKeyCandidates(const wxString &aHomeDir, const wxString &aHost)
Private keys to offer aHost, best first.
static std::string decoded(const std::string &aBase64)
std::string path
static std::vector< int > candidates(const SEGMENT_INDEX &aIndex, const SEG &aQuery, int aPadding)
VECTOR2I end