示例#1
0
        private static void ExecuteInternal(BrokeredMessage message, out BrokeredMessage response)
        {
            // We must set the out parameter no matter what, so do that first.
            response = new BrokeredMessage();

            // We'll create the app domain in the outer scope so we can unload it when we are finished (if it was created).
            AppDomain sandboxDomain = null;

            try
            {
                var requestId = (int)message.Properties["RequestId"];

                // Set correlation id of response message using the correlation ID of the request message.
                response.CorrelationId           = message.CorrelationId;
                response.Properties["RequestId"] = requestId;

                // The request will also load the associated route, so we'll use that feature
                // to reduce the number of SQL calls we make.
                var requestTask = TraceUtility.TraceTime("Get Load Request Task", () => Program.RequestRepository.GetRequestByIdAsync(requestId));
                TraceUtility.TraceTime("Load Request", () => requestTask.Wait());

                var request       = requestTask.Result;
                var route         = request.Route;
                var routeSettings = route.RouteSettings;

                // Trace the incoming request URI.
                Trace.TraceInformation("Trace 'Request Uri' - {0}", request.Uri);

                try
                {
                    var ev = new Evidence();
                    ev.AddHostEvidence(new Zone(SecurityZone.Internet));
                    var assemblyType         = typeof(ExecutionSandbox);
                    var assemblyPath         = Path.GetDirectoryName(assemblyType.Assembly.Location);
                    var sandboxPermissionSet = TraceUtility.TraceTime("Create Sandbox Permission Set", () => SecurityManager.GetStandardSandbox(ev));

                    // Exit with an error code if for some reason we can't get the sandbox permission set.
                    if (sandboxPermissionSet == null)
                    {
                        throw new EntryPointException("Unable to load the sandbox environment, please contact Subroute.io for help with this error.");
                    }

                    TraceUtility.TraceTime("Reconfigure Appropriate Permission Sets", () =>
                    {
                        // Remove access to UI components since we are in a headless environment.
                        sandboxPermissionSet.RemovePermission(typeof(UIPermission));

                        // Remove access to the File System Dialog since we are headless.
                        sandboxPermissionSet.RemovePermission(typeof(FileDialogPermission));

                        // Add the ability to use reflection for invocation and serialization.
                        sandboxPermissionSet.AddPermission(new ReflectionPermission(PermissionState.Unrestricted));

                        // Add the ability to make web requests.
                        sandboxPermissionSet.AddPermission(new WebPermission(PermissionState.Unrestricted));

                        // Add the ability to use the XmlSerializer and the DataContractSerializer.
                        sandboxPermissionSet.AddPermission(new SecurityPermission(SecurityPermissionFlag.SerializationFormatter));
                    });

                    // We'll create a new folder to hold an empty config file we create, and by
                    // doing this, it prevents the user from gaining access to our configuration
                    // file and the settings within, such as connection strings, infrastructure
                    // and other sensitive information we don't want them to have. Plus it will
                    // allow us to change any configuration settings that are specific to their
                    // application domain, such as default settings and other infrastructure.
                    // We must ensure that we have at least the root configuration XML tag in
                    // the configuration file we create or various dependencies will fail
                    // such as XmlSerializer and DataContractSerializer.
                    var tempDirectory       = Path.GetTempPath();
                    var userConfigDirectory = Path.Combine(tempDirectory, route.Uri);
                    var userConfigFilePath  = Path.Combine(userConfigDirectory, "app.config");
                    TraceUtility.TraceTime("Create Sandbox Directory", () => Directory.CreateDirectory(userConfigDirectory));

                    var configFile = TraceUtility.TraceTime("Generate App.Config File", () => routeSettings.Aggregate(@"<?xml version=""1.0"" encoding=""utf-8"" ?><configuration><appSettings>",
                                                                                                                      (current, setting) => current + $"<add key=\"{setting.Name}\" value=\"{setting.Value}\" />{Environment.NewLine}",
                                                                                                                      result => $"{result}</appSettings></configuration>"));

                    TraceUtility.TraceTime("Write App.Config to Disk", () => File.WriteAllText(userConfigFilePath, configFile));

                    // We'll add one last permission to allow the user access to their own private folder.
                    TraceUtility.TraceTime("Add Permission to Read App.Config File", () => sandboxPermissionSet.AddPermission(new FileIOPermission(FileIOPermissionAccess.Read, new[] { userConfigDirectory })));

                    TraceUtility.TraceTime("Create AppDomain", () =>
                    {
                        var appDomainSetup = new AppDomainSetup {
                            ApplicationBase = assemblyPath, ConfigurationFile = userConfigFilePath
                        };
                        sandboxDomain = AppDomain.CreateDomain("Sandboxed", ev, appDomainSetup, sandboxPermissionSet);
                    });

                    // The ExecutionSandbox is a MarshalByRef type that allows us to dynamically
                    // load their assemblies via a byte array and execute methods inside of
                    // their app domain from out full-trust app domain. It's the bridge that
                    // cross the app domain boundary.
                    var executionSandbox = TraceUtility.TraceTime("Create ExecutionSandbox Instance", () => (ExecutionSandbox)sandboxDomain.CreateInstance(
                                                                      assemblyType.Assembly.FullName,
                                                                      assemblyType.FullName,
                                                                      false,
                                                                      BindingFlags.Public | BindingFlags.Instance,
                                                                      null, null, null, null)
                                                                  .Unwrap());

                    // Build the ExecutionRequest object that represents the incoming request
                    // which holds the payload, headers, method, etc. The class is serialized
                    // so it can cross the app domain boundary. So it's serialized in our
                    // full-trust host app domain, and deserialized and reinstantiated in
                    // the sandbox app domain.
                    var uri = new Uri(request.Uri, UriKind.Absolute);
                    var executionRequest = TraceUtility.TraceTime("Create RouteRequest Instance", () => new RouteRequest(uri, request.Method)
                    {
                        IpAddress = request.IpAddress,
                        Headers   = HeaderHelpers.DeserializeHeaders(request.RequestHeaders),
                        Body      = request.RequestPayload
                    });

                    try
                    {
                        // The ExecutionSandbox we'll attempt to locate the best method to execute
                        // based on the incoming request method (GET, POST, DELETE, etc.) and
                        // will pass the ExecutionRequest we created above. In return, we receive
                        // an instance of ExecutionResponse that has been serialized like the request
                        // and deserialized in our full-trust host domain.
                        var executionResponse = TraceUtility.TraceTime("Load and Execute Route", () => executionSandbox.Execute(route.Assembly, executionRequest));

                        // We'll use the data that comes back from the response to fill out the
                        // remaineder of the database request record which will return the status
                        // code, message, payload, and headers. Then we update the database.
                        TraceUtility.TraceTime("Update Request Record", () =>
                        {
                            request.CompletedOn     = DateTimeOffset.UtcNow;
                            request.StatusCode      = (int)executionResponse.StatusCode;
                            request.StatusMessage   = executionResponse.StatusMessage;
                            request.ResponsePayload = executionResponse.Body;
                            request.ResponseHeaders = Common.RouteResponse.SerializeHeaders(executionResponse.Headers);

                            Program.RequestRepository.UpdateRequestAsync(request).Wait();
                        });

                        // We'll pass back a small bit of data indiciating to the subscribers of
                        // the response topic listening for our specific correlation ID that indicates
                        // the route code was executed successfully and to handle it as such.
                        response.Properties["Result"]  = (int)ExecutionResult.Success;
                        response.Properties["Message"] = "Completed Successfully";
                    }
                    catch (TargetInvocationException invokationException)
                    {
                        // These exceptions can occur when we encounter a permission exception where
                        // the user doesn't have permission to execute a particular block of code.
                        var securityException = invokationException.InnerException as SecurityException;
                        if (securityException != null)
                        {
                            throw new RoutePermissionException(GetPermissionErrorMessage(securityException), invokationException);
                        }

                        // Check for BadRequestException, we need to wrap it with the core exception.
                        // These exceptions can occur when query string parsing fails, and since the
                        // user's code doesn't have access to the core exceptions, we'll need to wrap
                        // it instead manually.
                        var badRequestException = invokationException.InnerException as Common.BadRequestException;
                        if (badRequestException != null)
                        {
                            throw new Core.Exceptions.BadRequestException(badRequestException.Message, badRequestException);
                        }

                        // Otherwise it is most likely a custom user exception.
                        throw new CodeException(invokationException.InnerException?.Message ?? "Route raised a custom exception.", invokationException.InnerException);
                    }
                    catch (EntryPointException entryPointException)
                    {
                        // These exceptions occur when an entry point could not be located.
                        // Since we don't have a reference to core in the common library.
                        // We'll instead wrap this exception in a core
                        // exception to apply a status code.
                        throw new RouteEntryPointException(entryPointException.Message, entryPointException);
                    }
                    catch (SecurityException securityException)
                    {
                        // These exceptions can occur when we encounter a permission exception where
                        // the user doesn't have permission to execute a particular block of code.
                        throw new RoutePermissionException(GetPermissionErrorMessage(securityException), securityException);
                    }
                    catch (Common.BadRequestException badRequestException)
                    {
                        // These exceptions can occur when query string parsing fails, and since the
                        // user's code doesn't have access to the core exceptions, we'll need to wrap
                        // it instead manually.
                        throw new Core.Exceptions.BadRequestException(badRequestException.Message, badRequestException);
                    }
                    catch (Exception routeException)
                    {
                        // These are all other exceptions that occur during the execution of
                        // a route. These exceptions are raised by the users code.
                        throw new RouteException(routeException.Message, routeException);
                    }
                }
                catch (Exception appDomainException)
                {
                    // This exception relates to exceptions configuring the AppDomain and we'll still notify the
                    // user, we just won't give them specific information that could reveal our infrastructure
                    // unless an IStatusCodeException was thrown, meaning it's a public exception.
                    var    statusCode          = 500;
                    var    statusMessage       = "An unexpected exception has occurred. Please contact Subroute.io regarding this error.";
                    var    statusCodeException = appDomainException as IStatusCodeException;
                    string stackTrace          = null;

                    if (statusCodeException != null)
                    {
                        statusCode    = (int)statusCodeException.StatusCode;
                        statusMessage = appDomainException.Message;

                        if (appDomainException is CodeException)
                        {
                            stackTrace = appDomainException.ToString();
                        }
                    }

                    request.CompletedOn     = DateTimeOffset.UtcNow;
                    request.StatusCode      = statusCode;
                    request.ResponsePayload = PayloadHelpers.CreateErrorPayload(statusMessage, stackTrace);
                    request.ResponseHeaders = HeaderHelpers.GetDefaultHeaders();

                    Program.RequestRepository.UpdateRequestAsync(request).Wait();

                    response.Properties["Result"]  = (int)ExecutionResult.Failed;
                    response.Properties["Message"] = appDomainException.Message;
                }
            }
            catch (Exception fatalException)
            {
                // These exceptions are absolutely fatal. We'll have to notify the waiting thread
                // via the service bus message, because we're unable to load a related request.
                response.Properties["Result"]  = (int)ExecutionResult.Fatal;
                response.Properties["Message"] = fatalException.Message;
            }
            finally
            {
                // Unload the users app domain to recover all memory used by it.
                if (sandboxDomain != null)
                {
                    TraceUtility.TraceTime("Unload AppDomain", () => AppDomain.Unload(sandboxDomain));
                }
            }
        }
示例#2
0
        private static async Task ExecuteInternalAsync(BrokeredMessage message, ICollector <BrokeredMessage> response)
        {
            // We'll always need a response message, so create it now and populate it below.
            var responseMessage = new BrokeredMessage();

            // We'll create the app domain in the outer scope so we can unload it when we are finished (if it was created).
            AppDomain sandboxDomain = null;

            try
            {
                var requestId = (int)message.Properties["RequestId"];

                // Set correlation id of response message using the correlation ID of the request message.
                responseMessage.CorrelationId           = message.CorrelationId;
                responseMessage.Properties["RequestId"] = requestId;

                // The request will also load the associated route, so we'll use that feature
                // to reduce the number of SQL calls we make.
                var request = await Program.RequestRepository.GetRequestByIdAsync(requestId).TraceTimeAsync("Load Request");

                var route         = request.Route;
                var routeSettings = route.RouteSettings.ToArray();
                var routePackages = route.RoutePackages.ToArray();

                // Trace the incoming request URI.
                Trace.TraceInformation("Trace 'Request Uri' - {0}", request.Uri);

                try
                {
                    var ev = new Evidence();
                    ev.AddHostEvidence(new Zone(SecurityZone.Internet));

                    var assemblyType         = typeof(ExecutionSandbox);
                    var assemblyPath         = Path.GetDirectoryName(assemblyType.Assembly.Location);
                    var sandboxPermissionSet = TraceUtility.TraceTime("Create Sandbox Permission Set",
                                                                      () => SecurityManager.GetStandardSandbox(ev));

                    // Exit with an error code if for some reason we can't get the sandbox permission set.
                    if (sandboxPermissionSet == null)
                    {
                        throw new EntryPointException("Unable to load the sandbox environment, please contact Subroute.io for help with this error.");
                    }

                    // We'll create a new folder to hold an empty config file we create, and by
                    // doing this, it prevents the user from gaining access to our configuration
                    // file and the settings within, such as connection strings, infrastructure
                    // and other sensitive information we don't want them to have. Plus it will
                    // allow us to change any configuration settings that are specific to their
                    // application domain, such as default settings and other infrastructure.
                    // We must ensure that we have at least the root configuration XML tag in
                    // the configuration file we create or various dependencies will fail
                    // such as XmlSerializer and DataContractSerializer.
                    var directories = TraceUtility.TraceTime("Setup Filesystem",
                                                             () => SetupFilesystem(route, routeSettings));

                    TraceUtility.TraceTime("Reconfigure Appropriate Permission Sets", () =>
                    {
                        // Remove access to UI components since we are in a headless environment.
                        sandboxPermissionSet.RemovePermission(typeof(UIPermission));

                        // Remove access to the File System Dialog since we are headless.
                        sandboxPermissionSet.RemovePermission(typeof(FileDialogPermission));

                        // Add the ability to use reflection for invocation and serialization.
                        sandboxPermissionSet.AddPermission(new ReflectionPermission(PermissionState.Unrestricted));

                        // Add the ability to make web requests.
                        sandboxPermissionSet.AddPermission(new WebPermission(PermissionState.Unrestricted));

                        // Add the ability to use the XmlSerializer and the DataContractSerializer.
                        // Also allows unmanaged code to be executed for drawing operations such as image resize and formatting.
                        sandboxPermissionSet.AddPermission(new SecurityPermission(SecurityPermissionFlag.SerializationFormatter | SecurityPermissionFlag.UnmanagedCode));

                        // Add permission to access the nuget package directory so that assemblies can be loaded.
                        sandboxPermissionSet.AddPermission(new FileIOPermission(FileIOPermissionAccess.PathDiscovery | FileIOPermissionAccess.Read, Settings.NugetPackageDirectory));

                        // Add permission to read execution temp directory.
                        sandboxPermissionSet.AddPermission(new FileIOPermission(FileIOPermissionAccess.Read, new[] { directories.RootDirectory, @"D:\GitHub\subroute.io\Subroute.Container\App_code\" }));
                    });

                    TraceUtility.TraceTime("Create AppDomain", () =>
                    {
                        var appDomainSetup = new AppDomainSetup {
                            ApplicationBase = assemblyPath, ConfigurationFile = directories.ConfigFile
                        };
                        sandboxDomain = AppDomain.CreateDomain("Sandboxed", ev, appDomainSetup, sandboxPermissionSet);
                    });

                    // The ExecutionSandbox is a MarshalByRef type that allows us to dynamically
                    // load their assemblies via a byte array and execute methods inside of
                    // their app domain from our full-trust app domain. It's the bridge that
                    // crosses the app domain boundary.
                    var executionSandbox = TraceUtility.TraceTime("Create ExecutionSandbox Instance",
                                                                  () => (ExecutionSandbox)sandboxDomain.CreateInstance(
                                                                      assemblyType.Assembly.FullName,
                                                                      assemblyType.FullName,
                                                                      false,
                                                                      BindingFlags.Public | BindingFlags.Instance,
                                                                      null, null, null, null)
                                                                  .Unwrap());

                    // Prepare packages by locating the proper assemblies for the current framework and ensure
                    // they have been downloaded to the packages folder and return their paths.
                    executionSandbox.SetReferences(await PreparePackagesAsync(routePackages).TraceTimeAsync("Prepare Packages"));

                    // To properly load assemblies into the dynamic partial trust assembly, we have to override
                    // the AssemblyResolve method which is only called when an assembly load attempt is made
                    // and fails. We can't use a closure here because to do that, the entire class ExecutionMethods
                    // would have to be serailized across the app domain boundry. So we'll add a string array property
                    // to the ExecutionSandbox class so we can access the references from in the app domain boundry.
                    // Just remember this event is executed inside the partial trust domain.
                    sandboxDomain.AssemblyResolve += (sender, args) =>
                    {
                        var name = new AssemblyName(args.Name);
                        var path = ExecutionSandbox.References.FirstOrDefault(r => Path.GetFileNameWithoutExtension(r) == name.Name);

                        return(path == null ? null : Assembly.LoadFrom(path));
                    };

                    // Build the ExecutionRequest object that represents the incoming request
                    // which holds the payload, headers, method, etc. The class is serialized
                    // so it can cross the app domain boundary. So it's serialized in our
                    // full-trust host app domain, and deserialized and reinstantiated in
                    // the sandbox app domain.
                    var uri = new Uri(request.Uri, UriKind.Absolute);
                    var executionRequest = TraceUtility.TraceTime("Create RouteRequest Instance",
                                                                  () => new RouteRequest(uri, request.Method)
                    {
                        IpAddress = request.IpAddress,
                        Headers   = HeaderHelpers.DeserializeHeaders(request.RequestHeaders),
                        Body      = request.RequestPayload
                    });

                    try
                    {
                        // The ExecutionSandbox we'll attempt to locate the best method to execute
                        // based on the incoming request method (GET, POST, DELETE, etc.) and
                        // will pass the ExecutionRequest we created above. In return, we receive
                        // an instance of ExecutionResponse that has been serialized like the request
                        // and deserialized in our full-trust host domain.
                        var executionResponse = TraceUtility.TraceTime("Load and Execute Request",
                                                                       () => executionSandbox.Execute(route.Assembly, executionRequest));

                        // We'll use the data that comes back from the response to fill out the
                        // remainder of the database request record which will return the status
                        // code, message, payload, and headers. Then we update the database.
                        request.CompletedOn     = DateTimeOffset.UtcNow;
                        request.StatusCode      = (int)executionResponse.StatusCode;
                        request.StatusMessage   = executionResponse.StatusMessage;
                        request.ResponsePayload = executionResponse.Body;
                        request.ResponseHeaders = RouteResponse.SerializeHeaders(executionResponse.Headers);

                        await Program.RequestRepository.UpdateRequestAsync(request).TraceTimeAsync("Update Request Record");

                        // We'll pass back a small bit of data indiciating to the subscribers of
                        // the response topic listening for our specific correlation ID that indicates
                        // the route code was executed successfully and to handle it as such.
                        responseMessage.Properties["Result"]  = (int)ExecutionResult.Success;
                        responseMessage.Properties["Message"] = "Completed Successfully";

                        // Create the response message and send it on its way.
                        response.Add(responseMessage);
                    }
                    catch (TargetInvocationException invokationException)
                    {
                        // These exceptions can occur when we encounter a permission exception where
                        // the user doesn't have permission to execute a particular block of code.
                        if (invokationException.InnerException is SecurityException securityException)
                        {
                            throw new RoutePermissionException(GetPermissionErrorMessage(securityException), invokationException);
                        }

                        // Check for BadRequestException, we need to wrap it with the core exception.
                        // These exceptions can occur when query string parsing fails, and since the
                        // user's code doesn't have access to the core exceptions, we'll need to wrap
                        // it instead manually.
                        if (invokationException.InnerException is BadRequestException badRequestException)
                        {
                            throw new Core.Exceptions.BadRequestException(badRequestException.Message, badRequestException);
                        }

                        // Otherwise it is most likely a custom user exception.
                        throw new CodeException(invokationException.InnerException?.Message ?? "Route raised a custom exception.", invokationException.InnerException);
                    }
                    catch (EntryPointException entryPointException)
                    {
                        // These exceptions occur when an entry point could not be located.
                        // Since we don't have a reference to core in the common library.
                        // We'll instead wrap this exception in a core
                        // exception to apply a status code.
                        throw new RouteEntryPointException(entryPointException.Message, entryPointException);
                    }
                    catch (SecurityException securityException)
                    {
                        // These exceptions can occur when we encounter a permission exception where
                        // the user doesn't have permission to execute a particular block of code.
                        throw new RoutePermissionException(GetPermissionErrorMessage(securityException), securityException);
                    }
                    catch (BadRequestException badRequestException)
                    {
                        // These exceptions can occur when query string parsing fails, and since the
                        // user's code doesn't have access to the core exceptions, we'll need to wrap
                        // it instead manually.
                        throw new Core.Exceptions.BadRequestException(badRequestException.Message, badRequestException);
                    }
                    catch (AggregateException asyncException) // Captures async and task exceptions.
                    {
                        // These exceptions occur when an entry point could not be located.
                        // Since we don't have a reference to core in the common library.
                        // We'll instead wrap this exception in a core
                        // exception to apply a status code.
                        if (asyncException.InnerException is EntryPointException entryPointException)
                        {
                            throw new RouteEntryPointException(entryPointException.Message, entryPointException);
                        }

                        // These exceptions can occur when we encounter a permission exception where
                        // the user doesn't have permission to execute a particular block of code.
                        if (asyncException.InnerException is SecurityException securityException)
                        {
                            throw new RoutePermissionException(GetPermissionErrorMessage(securityException), securityException);
                        }

                        // These exceptions can occur when query string parsing fails, and since the
                        // user's code doesn't have access to the core exceptions, we'll need to wrap
                        // it instead manually.
                        if (asyncException.InnerException is SecurityException badRequestException)
                        {
                            throw new Core.Exceptions.BadRequestException(badRequestException.Message, badRequestException);
                        }

                        // These are all other exceptions that occur during the execution of
                        // a route. These exceptions are raised by the users code.
                        throw new RouteException(asyncException.InnerException?.Message ?? asyncException.Message, asyncException.InnerException);
                    }
                    catch (Exception routeException)
                    {
                        // These are all other exceptions that occur during the execution of
                        // a route. These exceptions are raised by the users code.
                        throw new RouteException(routeException.Message, routeException);
                    }
                }
                catch (Exception appDomainException)
                {
                    // This exception relates to exceptions configuring the AppDomain and we'll still notify the
                    // user, we just won't give them specific information that could reveal our infrastructure
                    // unless an IStatusCodeException was thrown, meaning it's a public exception.
                    var    statusCode          = 500;
                    var    statusMessage       = "An unexpected exception has occurred. Please contact Subroute.io regarding this error.";
                    var    statusCodeException = appDomainException as IStatusCodeException;
                    string stackTrace          = null;

                    if (statusCodeException != null)
                    {
                        statusCode    = (int)statusCodeException.StatusCode;
                        statusMessage = appDomainException.Message;

                        if (appDomainException is CodeException)
                        {
                            stackTrace = appDomainException.ToString();
                        }
                    }

                    request.CompletedOn     = DateTimeOffset.UtcNow;
                    request.StatusCode      = statusCode;
                    request.ResponsePayload = PayloadHelpers.CreateErrorPayload(statusMessage, stackTrace);
                    request.ResponseHeaders = HeaderHelpers.GetDefaultHeaders();

                    await Program.RequestRepository.UpdateRequestAsync(request).TraceTimeAsync("Update Request Record (Error)");

                    responseMessage.Properties["Result"]  = (int)ExecutionResult.Failed;
                    responseMessage.Properties["Message"] = appDomainException.Message;

                    // Create the response message and send it on its way.
                    response.Add(responseMessage);
                }
            }
            catch (Exception fatalException)
            {
                // These exceptions are absolutely fatal. We'll have to notify the waiting thread
                // via the service bus message, because we're unable to load a related request.
                responseMessage.Properties["Result"]  = (int)ExecutionResult.Fatal;
                responseMessage.Properties["Message"] = fatalException.Message;

                // Create the response message and send it on its way.
                response.Add(responseMessage);
            }
            finally
            {
                // Unload the users app domain to recover all memory used by it.
                if (sandboxDomain != null)
                {
                    TraceUtility.TraceTime("Unload AppDomain",
                                           () => AppDomain.Unload(sandboxDomain));
                }
            }
        }