1 /*
2  * SessionOptions.cpp
3  *
4  * Copyright (C) 2021 by RStudio, PBC
5  *
6  * Unless you have received this program directly from RStudio pursuant
7  * to the terms of a commercial license agreement with RStudio, then
8  * this program is licensed to you under the terms of version 3 of the
9  * GNU Affero General Public License. This program is distributed WITHOUT
10  * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
11  * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
12  * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
13  *
14  */
15 
16 #include <session/SessionOptions.hpp>
17 
18 #include <boost/algorithm/string/trim.hpp>
19 
20 #include <boost/property_tree/ptree.hpp>
21 #include <boost/property_tree/ini_parser.hpp>
22 
23 #include <shared_core/FilePath.hpp>
24 #include <core/ProgramStatus.hpp>
25 #include <shared_core/SafeConvert.hpp>
26 #include <core/system/Crypto.hpp>
27 #include <core/system/System.hpp>
28 #include <core/system/Environment.hpp>
29 #include <core/system/Xdg.hpp>
30 
31 #include <shared_core/Error.hpp>
32 #include <core/Log.hpp>
33 
34 #include <core/r_util/RProjectFile.hpp>
35 #include <core/r_util/RUserData.hpp>
36 #include <core/r_util/RSessionContext.hpp>
37 #include <core/r_util/RActiveSessions.hpp>
38 #include <core/r_util/RVersionsPosix.hpp>
39 
40 #include <monitor/MonitorConstants.hpp>
41 
42 #include <r/session/RSession.hpp>
43 
44 #include <session/SessionConstants.hpp>
45 #include <session/SessionScopes.hpp>
46 #include <session/projects/SessionProjectSharing.hpp>
47 
48 #include "session-config.h"
49 
50 using namespace rstudio::core;
51 
52 namespace rstudio {
53 namespace session {
54 
55 namespace {
56 
ensureDefaultDirectory(std::string * pDirectory,const std::string & userHomePath)57 void ensureDefaultDirectory(std::string* pDirectory,
58                             const std::string& userHomePath)
59 {
60    if (*pDirectory != "~")
61    {
62       FilePath dir = FilePath::resolveAliasedPath(*pDirectory,
63                                                   FilePath(userHomePath));
64       Error error = dir.ensureDirectory();
65       if (error)
66       {
67          LOG_ERROR(error);
68          *pDirectory = "~";
69       }
70    }
71 }
72 
73 } // anonymous namespace
74 
options()75 Options& options()
76 {
77    static Options instance;
78    return instance;
79 }
80 
read(int argc,char * const argv[],std::ostream & osWarnings)81 core::ProgramStatus Options::read(int argc, char * const argv[], std::ostream& osWarnings)
82 {
83    using namespace boost::program_options;
84 
85    // get the shared secret
86    monitorSharedSecret_ = core::system::getenv(kMonitorSharedSecretEnvVar);
87    core::system::unsetenv(kMonitorSharedSecretEnvVar);
88 
89    // compute the resource path
90    Error error = core::system::installPath("..", argv[0], &resourcePath_);
91    if (error)
92    {
93       LOG_ERROR_MESSAGE("Unable to determine install path: "+error.getSummary());
94       return ProgramStatus::exitFailure();
95    }
96 
97    // detect running in OSX bundle and tweak resource path
98 #ifdef __APPLE__
99    if (resourcePath_.completePath("Info.plist").exists())
100       resourcePath_ = resourcePath_.completePath("Resources");
101 #endif
102 
103    // detect running in x86 directory and tweak resource path
104 #ifdef _WIN32
105    if (resourcePath_.completePath("x86").exists())
106    {
107       resourcePath_ = resourcePath_.getParent();
108    }
109 #endif
110 
111    // build options
112    options_description runTests("tests");
113    options_description runScript("script");
114    options_description verify("verify");
115    options_description version("version");
116    options_description program("program");
117    options_description log("log");
118    options_description docs("docs");
119    options_description www("www");
120    options_description session("session");
121    options_description allow("allow");
122    options_description r("r");
123    options_description limits("limits");
124    options_description external("external");
125    options_description git("git");
126    options_description user("user");
127    options_description misc("misc");
128    std::string saveActionDefault;
129    int sameSite;
130 
131    program_options::OptionsDescription optionsDesc =
132          buildOptions(&runTests, &runScript, &verify, &version, &program, &log, &docs, &www,
133                       &session, &allow, &r, &limits, &external, &git, &user, &misc,
134                       &saveActionDefault, &sameSite);
135 
136    addOverlayOptions(&misc);
137 
138    optionsDesc.commandLine.add(verify);
139    optionsDesc.commandLine.add(version);
140    optionsDesc.commandLine.add(runTests);
141    optionsDesc.commandLine.add(runScript);
142    optionsDesc.commandLine.add(program);
143    optionsDesc.commandLine.add(log);
144    optionsDesc.commandLine.add(docs);
145    optionsDesc.commandLine.add(www);
146    optionsDesc.commandLine.add(session);
147    optionsDesc.commandLine.add(allow);
148    optionsDesc.commandLine.add(r);
149    optionsDesc.commandLine.add(limits);
150    optionsDesc.commandLine.add(external);
151    optionsDesc.commandLine.add(git);
152    optionsDesc.commandLine.add(user);
153    optionsDesc.commandLine.add(misc);
154 
155    // define groups included in config-file processing
156    optionsDesc.configFile.add(program);
157    optionsDesc.configFile.add(log);
158    optionsDesc.configFile.add(docs);
159    optionsDesc.configFile.add(www);
160    optionsDesc.configFile.add(session);
161    optionsDesc.configFile.add(allow);
162    optionsDesc.configFile.add(r);
163    optionsDesc.configFile.add(limits);
164    optionsDesc.configFile.add(external);
165    optionsDesc.configFile.add(user);
166    optionsDesc.configFile.add(misc);
167 
168    // read configuration
169    ProgramStatus status = core::program_options::read(optionsDesc, argc,argv);
170    if (status.exit())
171       return status;
172 
173    // make sure the program mode is valid
174    if (programMode_ != kSessionProgramModeDesktop &&
175        programMode_ != kSessionProgramModeServer)
176    {
177       LOG_ERROR_MESSAGE("invalid program mode: " + programMode_);
178       return ProgramStatus::exitFailure();
179    }
180 
181    // resolve scope
182    scope_ = r_util::SessionScope::fromProjectId(projectId_, scopeId_);
183    scopeState_ = core::r_util::ScopeValid;
184 
185    sameSite_ = static_cast<rstudio::core::http::Cookie::SameSite>(sameSite);
186 
187    // call overlay hooks
188    resolveOverlayOptions();
189    std::string errMsg;
190    if (!validateOverlayOptions(&errMsg, osWarnings))
191    {
192       program_options::reportError(errMsg, ERROR_LOCATION);
193       return ProgramStatus::exitFailure();
194    }
195 
196    // compute program identity
197    programIdentity_ = "rsession-" + userIdentity_;
198 
199    // provide special home path in temp directory if we are verifying
200    bool isLauncherSession = getBoolOverlayOption(kLauncherSessionOption);
201    if (verifyInstallation_ && !isLauncherSession)
202    {
203       // we create a special home directory in server mode (since the
204       // user we are running under might not have a home directory)
205       // we do not do this for launcher sessions since launcher verification
206       // must be run as a specific user with the normal home drive setup
207       if (programMode_ == kSessionProgramModeServer)
208       {
209          verifyInstallationHomeDir_ = "/tmp/rstudio-verify-installation";
210          Error error = FilePath(verifyInstallationHomeDir_).ensureDirectory();
211          if (error)
212          {
213             LOG_ERROR(error);
214             return ProgramStatus::exitFailure();
215          }
216          core::system::setenv("R_USER", verifyInstallationHomeDir_);
217       }
218    }
219 
220    // resolve home directory from env vars
221    userHomePath_ = core::system::userHomePath("R_USER|HOME").getAbsolutePath();
222 
223    // use XDG data directory (usually ~/.local/share/rstudio, or LOCALAPPDATA
224    // on Windows) as the scratch path
225    userScratchPath_ = core::system::xdg::userDataDir().getAbsolutePath();
226 
227    // migrate data from old state directory to new directory
228    error = core::r_util::migrateUserStateIfNecessary(
229                programMode_ == kSessionProgramModeServer ?
230                    core::r_util::SessionTypeServer :
231                    core::r_util::SessionTypeDesktop);
232    if (error)
233    {
234       LOG_ERROR(error);
235    }
236 
237 
238    // set HOME if we are in standalone mode (this enables us to reflect
239    // R_USER back into HOME on Linux)
240    if (standalone())
241       core::system::setenv("HOME", userHomePath_);
242 
243    // ensure that default working dir and default project dir exist
244    ensureDefaultDirectory(&defaultWorkingDir_, userHomePath_);
245    ensureDefaultDirectory(&deprecatedDefaultProjectDir_, userHomePath_);
246 
247    // session timeout seconds is always -1 in desktop mode
248    if (programMode_ == kSessionProgramModeDesktop)
249       timeoutMinutes_ = 0;
250 
251    // convert string save action default to intenger
252    if (saveActionDefault == "yes")
253       saveActionDefault_ = r::session::kSaveActionSave;
254    else if (saveActionDefault == "no")
255       saveActionDefault_ = r::session::kSaveActionNoSave;
256    else if (saveActionDefault == "ask" || saveActionDefault.empty())
257       saveActionDefault_ = r::session::kSaveActionAsk;
258    else
259    {
260       program_options::reportWarnings(
261          "Invalid value '" + saveActionDefault + "' for "
262          "session-save-action-default. Valid values are yes, no, and ask.",
263          ERROR_LOCATION);
264       saveActionDefault_ = r::session::kSaveActionAsk;
265    }
266 
267    // convert relative paths by completing from the app resource path
268    resolvePath(resourcePath_, &rResourcesPath_);
269    resolvePath(resourcePath_, &wwwLocalPath_);
270    resolvePath(resourcePath_, &wwwSymbolMapsPath_);
271    resolvePath(resourcePath_, &coreRSourcePath_);
272    resolvePath(resourcePath_, &modulesRSourcePath_);
273    resolvePath(resourcePath_, &sessionLibraryPath_);
274    resolvePath(resourcePath_, &sessionPackageArchivesPath_);
275    resolvePostbackPath(resourcePath_, &rpostbackPath_);
276 #ifdef _WIN32
277    resolvePath(resourcePath_, &consoleIoPath_);
278    resolvePath(resourcePath_, &gnudiffPath_);
279    resolvePath(resourcePath_, &gnugrepPath_);
280    resolvePath(resourcePath_, &msysSshPath_);
281    resolvePath(resourcePath_, &sumatraPath_);
282    resolvePath(resourcePath_, &winutilsPath_);
283    resolvePath(resourcePath_, &winptyPath_);
284 
285    // winpty.dll lives next to rsession.exe on a full install; otherwise
286    // it lives in a directory named 32 or 64
287    core::FilePath pty(winptyPath_);
288    std::string completion;
289    if (pty.isWithin(resourcePath_))
290    {
291 #ifdef _WIN64
292       completion = "winpty.dll";
293 #else
294       completion = "x86/winpty.dll";
295 #endif
296    }
297    else
298    {
299 #ifdef _WIN64
300       completion = "64/bin/winpty.dll";
301 #else
302       completion = "32/bin/winpty.dll";
303 #endif
304    }
305    winptyPath_ = pty.completePath(completion).getAbsolutePath();
306 #endif // _WIN32
307    resolvePath(resourcePath_, &hunspellDictionariesPath_);
308    resolvePath(resourcePath_, &mathjaxPath_);
309    resolvePath(resourcePath_, &libclangHeadersPath_);
310    resolvePandocPath(resourcePath_, &pandocPath_);
311 
312    // rsclang
313    if (libclangPath_ != kDefaultRsclangPath)
314    {
315       libclangPath_ += "/5.0.2";
316    }
317    resolveRsclangPath(resourcePath_, &libclangPath_);
318 
319    // shared secret with parent
320    secret_ = core::system::getenv("RS_SHARED_SECRET");
321    /* SECURITY: Need RS_SHARED_SECRET to be available to
322       rpostback. However, we really ought to communicate
323       it in a more secure manner than this, at least on
324       Windows where even within the same user session some
325       processes can have different priviliges (integrity
326       levels) than others. For example, using a named pipe
327       with proper SACL to retrieve the shared secret, where
328       the name of the pipe is in an environment variable. */
329    //core::system::unsetenv("RS_SHARED_SECRET");
330 
331    // show user home page
332    showUserHomePage_ = core::system::getenv(kRStudioUserHomePage) == "1";
333    core::system::unsetenv(kRStudioUserHomePage);
334 
335    // multi session
336    multiSession_ = (programMode_ == kSessionProgramModeDesktop) ||
337                    (core::system::getenv(kRStudioMultiSession) == "1");
338 
339    // initial working dir override
340    initialWorkingDirOverride_ = core::system::getenv(kRStudioInitialWorkingDir);
341    core::system::unsetenv(kRStudioInitialWorkingDir);
342 
343    // initial environment file override
344    initialEnvironmentFileOverride_ = core::system::getenv(kRStudioInitialEnvironment);
345    core::system::unsetenv(kRStudioInitialEnvironment);
346 
347    // project sharing enabled
348    projectSharingEnabled_ =
349                 core::system::getenv(kRStudioDisableProjectSharing).empty();
350 
351    // initial project (can either be a command line param or via env)
352    r_util::SessionScope scope = sessionScope();
353    if (!scope.empty())
354    {
355         scopeState_ = r_util::validateSessionScope(
356                        scope,
357                        userHomePath(),
358                        userScratchPath(),
359                        session::projectIdToFilePath(userScratchPath(),
360                                  FilePath(getOverlayOption(
361                                        kSessionSharedStoragePath))),
362                        projectSharingEnabled(),
363                        &initialProjectPath_);
364    }
365    else
366    {
367       initialProjectPath_ = core::system::getenv(kRStudioInitialProject);
368       core::system::unsetenv(kRStudioInitialProject);
369    }
370 
371    // limit rpc client uid
372    limitRpcClientUid_ = -1;
373    std::string limitUid = core::system::getenv(kRStudioLimitRpcClientUid);
374    if (!limitUid.empty())
375    {
376       limitRpcClientUid_ = core::safe_convert::stringTo<int>(limitUid, -1);
377       core::system::unsetenv(kRStudioLimitRpcClientUid);
378    }
379 
380    // get R versions path
381    rVersionsPath_ = core::system::getenv(kRStudioRVersionsPath);
382    core::system::unsetenv(kRStudioRVersionsPath);
383 
384    // capture default R version environment variables
385    defaultRVersion_ = core::system::getenv(kRStudioDefaultRVersion);
386    core::system::unsetenv(kRStudioDefaultRVersion);
387    defaultRVersionHome_ = core::system::getenv(kRStudioDefaultRVersionHome);
388    core::system::unsetenv(kRStudioDefaultRVersionHome);
389 
390    // capture auth environment variables
391    authMinimumUserId_ = 0;
392    if (programMode_ == kSessionProgramModeServer)
393    {
394       authRequiredUserGroup_ = core::system::getenv(kRStudioRequiredUserGroup);
395       core::system::unsetenv(kRStudioRequiredUserGroup);
396 
397       authMinimumUserId_ = safe_convert::stringTo<unsigned int>(
398                               core::system::getenv(kRStudioMinimumUserId), 100);
399 
400 #ifndef _WIN32
401       r_util::setMinUid(authMinimumUserId_);
402 #endif
403       core::system::unsetenv(kRStudioMinimumUserId);
404    }
405 
406    // signing key - used for verifying incoming RPC requests
407    // in standalone mode
408    signingKey_ = core::system::getenv(kRStudioSigningKey);
409 
410    if (verifySignatures_)
411    {
412       // generate our own signing key to be used when posting back to ourselves
413       // this key is kept secret within this process and any child processes,
414       // and only allows communication from this rsession process and its children
415       error = core::system::crypto::generateRsaKeyPair(&sessionRsaPublicKey_, &sessionRsaPrivateKey_);
416       if (error)
417          LOG_ERROR(error);
418 
419       core::system::setenv(kRSessionRsaPublicKey, sessionRsaPublicKey_);
420       core::system::setenv(kRSessionRsaPrivateKey, sessionRsaPrivateKey_);
421    }
422 
423    // load cran options from repos.conf
424    FilePath reposFile(rCRANReposFile());
425    rCRANMultipleRepos_ = parseReposConfig(reposFile);
426 
427    // return status
428    return status;
429 }
430 
parseReposConfig(FilePath reposFile)431 std::string Options::parseReposConfig(FilePath reposFile)
432 {
433     using namespace boost::property_tree;
434 
435     if (!reposFile.exists())
436       return "";
437 
438    std::shared_ptr<std::istream> pIfs;
439    Error error = FilePath(reposFile).openForRead(pIfs);
440    if (error)
441    {
442       core::program_options::reportError("Unable to open repos file: " + reposFile.getAbsolutePath(),
443                   ERROR_LOCATION);
444 
445       return "";
446    }
447 
448    try
449    {
450       ptree pt;
451       ini_parser::read_ini(reposFile.getAbsolutePath(), pt);
452 
453       if (!pt.get_child_optional("CRAN"))
454       {
455          LOG_ERROR_MESSAGE("Repos file " + reposFile.getAbsolutePath() + " is missing CRAN entry.");
456          return "";
457       }
458 
459       std::stringstream ss;
460 
461       for (ptree::iterator it = pt.begin(); it != pt.end(); it++)
462       {
463          if (it != pt.begin())
464          {
465             ss << "|";
466          }
467 
468          ss << it->first << "|" << it->second.get_value<std::string>();
469       }
470 
471       return ss.str();
472    }
473    catch(const std::exception& e)
474    {
475       core::program_options::reportError(
476          "Error reading " + reposFile.getAbsolutePath() + ": " + std::string(e.what()),
477         ERROR_LOCATION);
478 
479       return "";
480    }
481 }
482 
getBoolOverlayOption(const std::string & name)483 bool Options::getBoolOverlayOption(const std::string& name)
484 {
485    std::string optionValue = getOverlayOption(name);
486    return boost::algorithm::trim_copy(optionValue) == "1";
487 }
488 
resolvePath(const FilePath & resourcePath,std::string * pPath)489 void Options::resolvePath(const FilePath& resourcePath,
490                           std::string* pPath)
491 {
492    if (!pPath->empty())
493       *pPath = resourcePath.completePath(*pPath).getAbsolutePath();
494 }
495 
496 #ifdef __APPLE__
497 
resolvePostbackPath(const FilePath & resourcePath,std::string * pPath)498 void Options::resolvePostbackPath(const FilePath& resourcePath,
499                                   std::string* pPath)
500 {
501    // On OSX we keep the postback scripts over in the MacOS directory
502    // rather than in the Resources directory -- make this adjustment
503    // when the default postback path has been passed
504    if (*pPath == kDefaultPostbackPath && programMode() == kSessionProgramModeDesktop)
505    {
506       FilePath path = resourcePath.getParent().completePath("MacOS/postback/rpostback");
507       *pPath = path.getAbsolutePath();
508    }
509    else
510    {
511       resolvePath(resourcePath, pPath);
512    }
513 }
514 
resolvePandocPath(const FilePath & resourcePath,std::string * pPath)515 void Options::resolvePandocPath(const FilePath& resourcePath,
516                                 std::string* pPath)
517 {
518    if (*pPath == kDefaultPandocPath && programMode() == kSessionProgramModeDesktop)
519    {
520       FilePath path = resourcePath.getParent().completePath("MacOS/pandoc");
521       *pPath = path.getAbsolutePath();
522    }
523    else
524    {
525       resolvePath(resourcePath, pPath);
526    }
527 }
528 
resolveRsclangPath(const FilePath & resourcePath,std::string * pPath)529 void Options::resolveRsclangPath(const FilePath& resourcePath,
530                                  std::string* pPath)
531 {
532    if (*pPath == kDefaultRsclangPath && programMode() == kSessionProgramModeDesktop)
533    {
534       FilePath path = resourcePath.getParent().completePath("MacOS/rsclang");
535       *pPath = path.getAbsolutePath();
536    }
537    else
538    {
539       resolvePath(resourcePath, pPath);
540    }
541 }
542 
543 #else
544 
resolvePostbackPath(const FilePath & resourcePath,std::string * pPath)545 void Options::resolvePostbackPath(const FilePath& resourcePath,
546                                   std::string* pPath)
547 {
548    resolvePath(resourcePath, pPath);
549 }
550 
resolvePandocPath(const FilePath & resourcePath,std::string * pPath)551 void Options::resolvePandocPath(const FilePath& resourcePath,
552                                   std::string* pPath)
553 {
554    resolvePath(resourcePath, pPath);
555 }
556 
resolveRsclangPath(const FilePath & resourcePath,std::string * pPath)557 void Options::resolveRsclangPath(const FilePath& resourcePath,
558                                  std::string* pPath)
559 {
560    resolvePath(resourcePath, pPath);
561 }
562 #endif
563 
564 } // namespace session
565 } // namespace rstudio
566