Giter Club home page Giter Club logo

twilio-aspnet's Introduction

Twilio helper library for ASP.NET

Build

The Twilio helper library for ASP.NET (Twilio.AspNet), helps you integrate the official Twilio SDK for C# and .NET into your ASP.NET applications. The library supports ASP.NET MVC on .NET Framework and ASP.NET Core.

This library helps you respond to webhooks, adds the Twilio client to the dependency injection container, and validate HTTP request originate from Twilio.

Twilio.AspNet.Core

NuGet Badge

Requirements

Requires .NET 6.0 or later.

Installation

Run the following command to install the package using the .NET CLI:

dotnet add package Twilio.AspNet.Core

Alternatively, from the Package Manager Console or Developer PowerShell, run the following command to install the latest version:

Install-Package Twilio.AspNet.Core

Alternatively, use the NuGet Package Manager for Visual Studio or the NuGet window for JetBrains Rider, then search for Twilio.AspNet.Core and install the package.

Twilio.AspNet.Mvc

NuGet Badge

Requirements

Requires .NET 4.6.2 or later.

Installation

From the Package Manager Console or Developer PowerShell, run the following command to install the latest version:

Install-Package Twilio.AspNet.Mvc

Alternatively, use the NuGet Package Manager for Visual Studio or the NuGet window for JetBrains Rider, then search for Twilio.AspNet.Mvc and install the package.

Code Samples for either Library

Incoming SMS

using Twilio.AspNet.Common;
using Twilio.AspNet.Core; // or .Mvc for .NET Framework
using Twilio.TwiML;

public class SmsController : TwilioController
{
    // GET: Sms
    public TwiMLResult Index(SmsRequest request)
    {
        var response = new MessagingResponse();
        response.Message($"Ahoy {request.From}!");
        return TwiML(response);
    }
}

This controller will handle the SMS webhook. The details of the incoming SMS will be bound to the SmsRequest request parameter. By inheriting from the TwilioController, you get access to the TwiML method which you can use to respond with TwiML.

Incoming Voice Call

using Twilio.AspNet.Common;
using Twilio.AspNet.Core; // or .Mvc for .NET Framework
using Twilio.TwiML;

public class VoiceController : TwilioController
{
    // GET: Voice
    public TwiMLResult Index(VoiceRequest request)
    {
        var response = new VoiceResponse();
        response.Say($"Ahoy! Are you from {request.FromCity}?");
        return TwiML(response);
    }
}

This controller will handle the Voice webhook. The details of the incoming voice call will be bound to the VoiceRequest request parameter. By inheriting from the TwilioController, you get access to the TwiML method which you can use to respond with TwiML.

Using TwiML extension methods instead of inheriting from TwilioController

If you can't inherit from the TwilioController class, you can use the TwiML extension methods.

using Microsoft.AspNetCore.Mvc; // or System.Web.Mvc for .NET Framework
using Twilio.AspNet.Common;
using Twilio.AspNet.Core; // or .Mvc for .NET Framework
using Twilio.TwiML;

public class SmsController : Controller
{
    // GET: Sms
    public TwiMLResult Index(SmsRequest request)
    {
        var response = new MessagingResponse();
        response.Message($"Ahoy {request.From}!");
        return this.TwiML(response);
    }
}

This sample is the same as the previous SMS webhook sample, but instead of inheriting from TwilioController, the SmsController inherits from the ASP.NET MVC provided Controller, and uses this.TwiML to use the TwiML extension method.

Minimal API

Twilio.AspNet.Core also has support for Minimal APIs.

This sample shows you how you can hande an SMS webhook using HTTP GET and POST.

using Microsoft.AspNetCore.Mvc;
using Twilio.AspNet.Core.MinimalApi;
using Twilio.TwiML;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/sms", ([FromQuery] string from) =>
{
    var response = new MessagingResponse();
    response.Message($"Ahoy {from}!");
    return Results.Extensions.TwiML(response);
});

app.MapPost("/sms", async (HttpRequest request) =>
{
    var form = await request.ReadFormAsync();
    var from = form["from"];
    response.Message($"Ahoy {from}!");
    return Results.Extensions.TwiML(response);
});

app.Run();

In traditional MVC controllers, the SmsRequest, VoiceRequest, and other typed request object would be bound, but Minimal APIs does not support the same model binding.

Instead, you can bind individual parameters for HTTP GET requests using the FromQuery attribute. When you don't specify the FromQuery attribute, multiple sources will be considered to bind from in addition to the query string parameters. For HTTP POST requests you can grab the form and then retrieve individual parameters by string index.
To respond with TwiML, use the Results.Extensions.TwiML method.

Model Bind webhook requests to typed .NET objects

Twilio.AspNet comes with multiple classes to help you bind the data from webhooks to strongly typed .NET objects. Here's the list of classes:

  • SmsRequest: Holds data for incoming SMS webhook requests
  • SmsStatusCallbackRequest: Holds data for tracking the delivery status of an outbound Twilio SMS or MMS
  • StatusCallbackRequest: Holds data for tracking the status of an outbound Twilio Voice Call
  • VoiceRequest: Holds data for incoming Voice Calls

Note Only MVC Controllers and Razor Pages support model binding to typed .NET objects. In Minimal APIs and other scenarios, you'll have to write code to extract the parameters yourself.

The following sample shows how to accept inbound SMS, respond, and track the status of the SMS response.

using Twilio.AspNet.Common;
using Twilio.AspNet.Core; // or .Mvc for .NET Framework
using Twilio.TwiML;

public class SmsController : TwilioController
{
    private readonly ILogger<SmsController> logger;

    public SmsController(ILogger<SmsController> logger)
    {
        this.logger = logger;
    }

    public TwiMLResult Index(SmsRequest request)
    {
        var messagingResponse = new MessagingResponse();
        messagingResponse.Message(
            body: $"Ahoy {request.From}!",
            action: new Uri("/Sms/StatusCallback"),
            method: Twilio.Http.HttpMethod.Post
        );
        return TwiML(messagingResponse);
    }

    public void StatusCallback(SmsStatusCallbackRequest request)
    {
        logger.LogInformation("SMS Status: {Status}", request.MessageStatus);
    }
}

As shown in the sample above, you can add an SmsRequest as a parameter, and MVC will bind the object for you. The code then responds with an SMS with the status and method parameter. When the status of the SMS changes, Twilio will send an HTTP POST request to StatusCallback action. You can add an SmsStatusCallbackRequest as a parameter, and MVC will bind the object for you.

Add the Twilio client to the ASP.NET Core dependency injection container

In ASP.NET Core, you can add the Twilio REST API client to ASP.NET Core's services using the .AddTwilioClient method, like this:

using Twilio.AspNet.Core;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddTwilioClient()

Now you can request ITwilioRestClient and TwilioRestClient via dependency injection.

You can configure the Twilio client using the following configuration:

{
  "Twilio": {
    "AuthToken": "[YOUR_AUTH_TOKEN]",
    "Client": {
      "AccountSid": "[YOUR_ACCOUNT_SID]",
      "AuthToken": "[YOUR_AUTH_TOKEN]",
      "ApiKeySid": "[YOUR_API_KEY_SID]",
      "ApiKeySecret": "[YOUR_API_KEY_SECRET]",
      "CredentialType": "[Unspecified|AuthToken|ApiKey]",
      "Region": null,
      "Edge": null,
      "LogLevel": null
    }
  }
}

A couple of notes:

  • Twilio:Client:AuthToken falls back on Twilio:AuthToken. You only need to configure one of them.
  • Twilio:Client:CredentialType has the following valid values: Unspecified, AuthToken, or ApiKey.
  • Twilio:Client:CredentialType is optional and defaults to Unspecified. If Unspecified, whether you configured an API key or an Auth Token will be detected.

If you do not wish to configure the Twilio client using .NET configuration, you can do so manually:

using Twilio.AspNet.Core;

var builder = WebApplication.CreateBuilder(args);

builder.Services
  .AddTwilioClient((serviceProvider, options) =>
  {
    options.AccountSid = "[YOUR_ACCOUNT_SID]";
    options.AuthToken = "[YOUR_AUTH_TOKEN]";
    options.ApiKeySid = "[YOUR_API_KEY_SID]";
    options.ApiKeySecret = "[YOUR_API_KEY_SECRET]";
    options.Edge = null;
    options.Region = null;
    options.LogLevel = null;
    options.CredentialType = CredentialType.Unspecified;
  });

Warning Do not hard-code your Auth Token or API key secret into code and do not check them into source control. We recommend using the Secrets Manager for local development. Alternatively, you can use environment variables, a vault service, or other more secure techniques.

Customize the HTTP client

By default when you call .AddTwilioClient, an HTTP client factory is configured that is used to provide an HttpClient to the Twilio REST client. If you'd like to customize this HTTP client, you can do so by overriding the "Twilio" HTTP client factory, after invoking .AddTwilioClient:

builder.Services.AddTwilioClient();
builder.Services.AddHttpClient("Twilio")
    .ConfigureHttpClient(client =>
    {
        client.BaseAddress = new Uri("YOUR_PROXY_ADDRESS");
    })
    .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
    {
        // same options as the Twilio C# SDK
        AllowAutoRedirect = false
    });

Validate Twilio HTTP requests

Webhooks require your endpoint to be publicly available, but this also introduces the risk that bad actors could find your webhook URL and try to abuse it.

Luckily, you can verify that an HTTP request originated from Twilio. The Twilio.AspNet library provides an attribute that will validate the request for you in MVC. The implementation differs between the Twilio.AspNet.Core and Twilio.AspNet.Mvc library.

Validate requests in ASP.NET Core

Add the .AddTwilioRequestValidation method at startup:

using Twilio.AspNet.Core;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddTwilioRequestValidation();

Then configure the request validation:

{
  "Twilio": {
    "AuthToken": "[YOUR_AUTH_TOKEN]",
    "RequestValidation": {
      "AuthToken": "[YOUR_AUTH_TOKEN]",
      "AllowLocal": false,
      "BaseUrlOverride": "https://??????.ngrok.io"
    }
  }
}

A couple of notes about the configuration:

  • Twilio:RequestValidation:AuthToken falls back on Twilio:AuthToken. You only need to configure one of them.
  • AllowLocal will skip validation when the HTTP request originated from localhost. ⚠️ Only use this during development, as this will make your application vulnerable to Server-Side Request Forgery.
  • Use BaseUrlOverride in case your app is behind a reverse proxy or a tunnel like ngrok. The path of the current request will be appended to the BaseUrlOverride for request validation.

Info Instead of configuring the BaseUrlOverride, you can use the forwarded headers middleware to set the correct scheme, port, host, etc. on the current HTTP request.

using Microsoft.AspNetCore.HttpOverrides;
using Twilio.AspNet.Core;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddTwilioRequestValidation();
builder.Services.Configure<ForwardedHeadersOptions>(options => options.ForwardedHeaders = ForwardedHeaders.All);
// more service configuration

var app = builder.Build();

app.UseForwardedHeaders();

// more request pipeline configuration

app.Run();

As a result, you don't have to configure BaseUrlOverride whenever you restart ngrok, or change reverse proxy URLs. Follow Microsoft's guidance to configure the forwarded header middleware securely.

You can also manually configure the request validation:

using Twilio.AspNet.Core;

var builder = WebApplication.CreateBuilder(args);

builder.Services
  .AddTwilioRequestValidation((serviceProvider, options) =>
  {
    options.AuthToken = "[YOUR_AUTH_TOKEN]";
    options.AllowLocal = false;
    options.BaseUrlOverride = "https://??????.ngrok.io";
  });

Warning Do not hard-code your Auth Token into code and do not check them into source control. We recommend using the Secrets Manager for local development. Alternatively, you can use environment variables, a vault service, or other more secure techniques.

ASP.NET Core MVC

Once request validation has been configured, you can use the [ValidateRequest] attribute in MVC. You can apply the attribute globally, to MVC areas, controllers, and actions. Here's an example where the attribute is applied to the Index action:

using Twilio.AspNet.Common;
using Twilio.AspNet.Core;
using Twilio.TwiML;

public class SmsController : TwilioController
{
    [ValidateRequest]
    public TwiMLResult Index(SmsRequest request)
    {
        var response = new MessagingResponse();
        response.Message("Ahoy!");
        return TwiML(response);
    }
}
ASP.NET Core Endpoints & Minimal APIs

.NET 7 introduces the concept of endpoint filters which you can apply to any ASP.NET Core endpoint. The helper library for ASP.NET Core added an endpoint filter to validate Twilio requests called ValidateTwilioRequestFilter.

You can add this filter to any endpoint or endpoint group using the ValidateTwilioRequest method:

// add filter to endpoint
app.MapPost("/sms", () => ...)
    .ValidateTwilioRequest();
    
// add filter to endpoint group
var twilioGroup = app.MapGroup("/twilio");
twilioGroup.ValidateTwilioRequest();
twilioGroup.MapPost("/sms", () => ...);
twilioGroup.MapPost("/voice", () => ...);

Alternatively, you can add the endpoint filter yourself:

app.MapPost("/sms", () => ...)
    .AddEndpointFilter<ValidateTwilioRequestFilter>();
ASP.NET Core Middleware

When you can't use the [ValidateRequest] filter or ValidateTwilioRequestFilter, you can use the ValidateTwilioRequestMiddleware instead. You can add add the ValidateTwilioRequestFilter like this:

app.UseTwilioRequestValidation();
// or the equivalent: app.UseMiddleware<ValidateTwilioRequestMiddleware>();

This middleware will perform the validation for all requests. If you don't want to apply the validation to all requests, you can use app.UseWhen() to run the middleware conditionally.

Here's an example of how to validate requests that start with path /twilio-media, as to protect media files that only the Twilio Proxy should be able to access:

using System.Net;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Options;
using Twilio.AspNet.Core;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddTwilioRequestValidation();

var app = builder.Build();

app.UseWhen(
    context => context.Request.Path.StartsWithSegments("/twilio-media", StringComparison.OrdinalIgnoreCase),
    app => app.UseTwilioRequestValidation()
);

app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new PhysicalFileProvider(Path.Combine(builder.Environment.ContentRootPath, "TwilioMedia")),
    RequestPath = "/twilio-media"
});

app.Run();

Validate requests in ASP.NET MVC on .NET Framework

In your Web.config you can configure request validation like shown below:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <configSections>
      <sectionGroup name="twilio" type="Twilio.AspNet.Mvc.TwilioSectionGroup,Twilio.AspNet.Mvc">
        <section name="requestValidation" type="Twilio.AspNet.Mvc.RequestValidationConfigurationSection,Twilio.AspNet.Mvc"/>
      </sectionGroup>
    </configSections>
    <twilio>
       <requestValidation 
         authToken="[YOUR_AUTH_TOKEN]"
         baseUrlOverride="https://??????.ngrok.io"
         allowLocal="false"
       />
    </twilio>
</configuration>

You can also configure request validation using app settings:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <appSettings>
      <add key="twilio:requestValidation:authToken" value="[YOUR_AUTH_TOKEN]"/>
      <add key="twilio:requestValidation:baseUrlOverride" value="https://??????.ngrok.io"/>
      <add key="twilio:requestValidation:allowLocal" value="false"/>
    </appSettings>
</configuration>

If you configure request validation using both ways, app setting will overwrite the twilio/requestValidation configuration element.

A couple of notes about the configuration:

  • allowLocal will skip validation when the HTTP request originated from localhost. ⚠️ Only use this during development, as this will make your application vulnerable to Server-Side Request Forgery.
  • Use baseUrlOverride in case you are in front of a reverse proxy or a tunnel like ngrok. The path of the current request will be appended to the baseUrlOverride for request validation.

Warning Do not hard-code your Auth Token into code and do not check them into source control. Use the UserSecretsConfigBuilder for local development or one of the other configuration builders. Alternatively, you should encrypt the configuration sections containing secrets like the Auth Token.

Now that request validation has been configured, use the [ValidateRequest] attribute. You can apply the attribute globally, to MVC areas, controllers, and actions. Here's an example where the attribute is applied to the Index action:

using Twilio.AspNet.Common;
using Twilio.AspNet.Mvc;
using Twilio.TwiML;

public class SmsController : TwilioController
{
    [ValidateRequest]
    public TwiMLResult Index(SmsRequest request)
    {
        var response = new MessagingResponse();
        response.Message("Ahoy!");
        return TwiML(response);
    }
}

Validate requests using the RequestValidationHelper

The [ValidateRequest] attribute only works for MVC. If you need to validate requests outside of MVC, you can use the RequestValidationHelper class provided by Twilio.AspNet. Alternatively, the RequestValidator class from the Twilio SDK can also help you with this.

Here's a Minimal API example that validates the incoming request originated from Twilio:

using System.Net;
using Microsoft.Extensions.Options;
using Twilio.AspNet.Core;
using Twilio.AspNet.Core.MinimalApi;
using Twilio.TwiML;

var builder = WebApplication.CreateBuilder(args);

// adds TwilioRequestValidationOptions
builder.Services.AddTwilioRequestValidation();

var app = builder.Build();

app.MapPost("/sms", (HttpContext httpContext) =>
{
    if (IsValidTwilioRequest(httpContext) == false)
        return Results.StatusCode((int) HttpStatusCode.Forbidden);

    var messagingResponse = new MessagingResponse();
    messagingResponse.Message("Ahoy!");
    return Results.Extensions.TwiML(messagingResponse);
});

app.Run();

bool IsValidTwilioRequest(HttpContext httpContext)
{
    var options = httpContext.RequestServices
        .GetService<IOptions<TwilioRequestValidationOptions>>()
        ?.Value ?? throw new Exception("TwilioRequestValidationOptions missing.");

    string? urlOverride = null;
    if (options.BaseUrlOverride != null)
    {
        var request = httpContext.Request;
        urlOverride = $"{options.BaseUrlOverride.TrimEnd('/')}{request.Path}{request.QueryString}";
    }

    return RequestValidationHelper.IsValidRequest(httpContext, options.AuthToken, urlOverride, options.AllowLocal);
}

AddTwilioRequestValidation adds the TwilioRequestValidationOptions, which is normally used for the [ValidateRequest] attribute, but in this sample it is used to retrieve the request validation configuration yourself. Then inside of the /sms endpoint, the IsValidTwilioRequest method is used to validate the request. IsValidTwilioRequest retrieves the request validation options from DI and performs the same logic as [ValidateRequest] would do for MVC requests. If the request is not valid, HTTP Status Code 403 is returned, otherwise, some TwiML is returned to Twilio.

twilio-aspnet's People

Contributors

atbaker avatar dependabot[bot] avatar dprothero avatar eddiezane avatar giorgi avatar joelweiss avatar joliveros avatar keltex avatar matthartz avatar michaelsl avatar rhegner avatar swimburger avatar tofer avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

twilio-aspnet's Issues

Use Forwarded headers for request validation when available

When in front of a reverse proxy or a tunnel like ngrok, we currently have to set the BaseUrlOverride setting, but we could make skip this extra step if the request validation helper would take the Forwarded headers into consideration.

There are many of these headers, so we'd need to figure out which to use and not to use.

Not having to update the BaseUrlOverride anytime we restart ngrok would be a great improvement to the developer experience.

Brazilian characters broking TwiML method

It seems that every time that I have some Brazilian characters and words, like for example "não" which means "no", the static method LoadFromString which returns a XDocument, gets a non expected Exception.

Path:
TwilioController->TwiML(MessagingResponse response)->TwiMLResult->LoadFromString(xx)

Verify and fix public `TwilioController` methods

Previously, a pull request (#21) was merged to fix issues with Swagger UI.
This PR made the TwiML methods protected, but I made them public so they could be tested.

We should verify whether this is still an issue with Swagger UI and if so, find a different solution, so the methods can stay testable while also preventing issues with Swagger UI.

I'll assign this to myself for now and check it out when I find time.

Remove deprecated Microsoft.AspNetCore.Mvc.Core dependency

The Twilio.AspNet.Core library depends on a long obsolete version of the Microsoft.AspNetCore.Mvc.Core library

[email protected]
  -> Microsoft.AspNetCore.Mvc.Core >= 1.0.4
        -> Microsoft.AspNetCore.Http >= 1.0.3

This obsolete dependency is vulnerable to CVE-2020-1045, and presents a roadblock to anyone using this package with a project that is under static application security testing (SAST) and other vulnerability detection schemes (like GitHub dependabot).

The nuget.org page for [email protected] states:

This package has been deprecated as part of the .NET Package Deprecation effort. You can learn more about it from dotnet/announcements#217

Exception when creating a default client AddTwilioClient

This is running under .net 6 (google cloud run). I'm getting numerous instances of this exception (see below). I think what's happening here is you have a function ChangeEmptyStringToNull which modifies the IConfigurationSection. It's called recursively to convert any empty strings to nulls. I assume this is because somewhere you check for null rather than IsNullOrEmpty in your configuration. That code can be found here:

https://github.com/twilio-labs/twilio-aspnet/blob/93423276a3e05ea031c559eaba10d3adbbc24c96/src/Twilio.AspNet.Core/TwilioClientDependencyInjectionExtensions.cs

I think the better approach here would be to either in the Validate() function check for empty strings or just allow for them elsewhere (not sure where) and do IsStringEmptyOrNull checks instead of if (s==null). Perhaps in TwilioRestClient CreateTwilioClient(IServiceProvider provider)

System.InvalidOperationException: Operations that change non-concurrent collections must have exclusive access. A concurrent update was performed on this collection and corrupted its state. The collection's state is no longer correct. at System.Collections.Generic.Dictionary2.FindValue(TKey key)
at System.Collections.Generic.Dictionary2.TryGetValue(TKey key, TValue& value) at Microsoft.Extensions.Configuration.ConfigurationRoot.GetConfiguration(IList1 providers, String key)
at Microsoft.Extensions.Configuration.ChainedConfigurationProvider.TryGet(String key, String& value)
at Microsoft.Extensions.Configuration.ConfigurationRoot.GetConfiguration(IList1 providers, String key) at Twilio.AspNet.Core.TwilioClientDependencyInjectionExtensions.ChangeEmptyStringToNull(IConfigurationSection configSection) at Twilio.AspNet.Core.TwilioClientDependencyInjectionExtensions.ChangeEmptyStringToNull(IConfigurationSection configSection) at Twilio.AspNet.Core.TwilioClientDependencyInjectionExtensions.<>c.<ConfigureDefaultOptions>b__6_0(TwilioClientOptions opts, IConfiguration config) at Microsoft.Extensions.Options.OptionsFactory1.Create(String name)
at System.Lazy1.ViaFactory(LazyThreadSafetyMode mode) at System.Lazy1.ExecutionAndPublication(LazyHelper executionAndPublication, Boolean useDefaultConstructor)
at System.Lazy1.CreateValue() at Microsoft.Extensions.Options.OptionsManager1.Get(String name)
at Twilio.AspNet.Core.TwilioClientDependencyInjectionExtensions.CreateTwilioClient(IServiceProvider provider)`

Update library to use .NET Standard 2.0

Microsoft recommends targeting .NET Standard 2.0, which will support these versions of .NET:

.NET implementation Version support
.NET and .NET Core 2.0, 2.1, 2.2, 3.0, 3.1, 5.0, 6.0
.NET Framework 1 4.6.1, 4.6.2, 4.7, 4.7.1, 4.7.2, 4.8
Mono 5.4, 6.4
Xamarin.iOS 10.14, 12.16
Xamarin.Mac 3.8, 5.16
Xamarin.Android 8.0, 10.0
Universal Windows Platform 10.0.16299, TBD
Unity 2018.1

Everything before .NET Framework 4.6.2, and before .NET Core 3.0 (and 5.0) is not supported by Microsoft anymore.

I would recommend moving our targets to .NET Standard 2.0 where possible. It will simplify our builds because we'll have to use less conditional compilation and we'll be able to consume more NuGet packages as most require .NET Standard 2.0 as well.

Dependency on .NET Standard 1.6.1

I was trying to utilize the Twilio.ASPNet.Core library in my Lambda function, but when I went to publish it to Lambda, I was informed that it has a dependency on .NET Standard 1.6.1, and the max version supported by netcoreapp 1.0 is 1.6.0. Lambda only supports .NET Core 1.0, which is what this library says is the lowest version it supports. Is there a trick in the .csproj file or something I can do to make this only use .NET Standard 1.6.0?

Make builds deterministic and add source link

By making builds detirministic, the output should always be the same when the input of the build is the same.
This makes it easier to verify the integraty of the package.

Source link makes it easier for users to debug the library from their IDE by letting them use step-by-step debugging into the library without having to clone the project locally

Add `authTokenSettingName` parameter to `ValidateRequestAttribute`

Currently, to use ValidateRequestAttribute, you have to pass the Twilio auth token to the authToken parameter.
This can only be done by hard-coding the token into the parameter or into a const string and passing the const to the parameter, either of which result in embedding the auth token into your assembly.
Hard-coding the auth token is a bad practice.

IMO, we should offer an alternative and mark the existing parameter as obsolete.
I suggest we add a new parameter authTokenSettingName which will

  • for .NET Framework, look up the auth token in AppSettings.
  • for .NET (Core), look up the auth token from IConfiguration

An alternative solution for .NET Core would be to add code to the startup class which will configure the auth token using the Options pattern.

Both these options seem like sensible defaults to me, and if the user wants a different implementation, they can inherit from ValidateRequestAttribute and add their own logic to it.

VoiceRequest is missing the ParentCallSid param

According to the https://www.twilio.com/docs/voice/twiml#request-parameters ParentCallSid param can be sent in the VoiceRequest. I believe it's important to have it there, because the StatusCallbackRequest which inherits that request always contains it.

`
/// < summary >
/// A unique identifier for the call that created this leg. This parameter is not passed if this is the first leg of a call.
/// </ summary >

public string ParentCallSid { get; set; }

`

image

Expose formatting option for TwiML extension methods

TwiML.ToString accepts an optional parameter SaveOptions formattingOptions = SaveOptions.None.
I think we should expose the same or a similar option to the TwiML extension methods in this library.

I'm leaning towards using our own enum so user's don't have to import the System.Xml.Linq namespace.

MMS?

I don't see any support here for receiving/processing/sending MMS messages. And I see references in Twilio's docs to receiving MMS but no actual code samples anywhere?

Machine detection

It seems that VoiceRequest lacks answered by information. Or can it be found elsewhere?

Unable to Install Twilio.AspNet.Mvc

Attempting to gather dependency information for package 'Twilio.AspNet.Mvc.5.0.1' with respect to project 'PbPresortServices', targeting '.NETFramework,Version=v4.0' Attempting to resolve dependencies for package 'Twilio.AspNet.Mvc.5.0.1' with DependencyBehavior 'Lowest' Resolving actions to install package 'Twilio.AspNet.Mvc.5.0.1' Resolved actions to install package 'Twilio.AspNet.Mvc.5.0.1' Attempting to gather dependency information for package 'Twilio.AspNet.Mvc.5.0.1' with respect to project 'PbPresortServices.Web.Mvc', targeting '.NETFramework,Version=v4.0' Attempting to resolve dependencies for package 'Twilio.AspNet.Mvc.5.0.1' with DependencyBehavior 'Lowest' Resolving actions to install package 'Twilio.AspNet.Mvc.5.0.1' Resolved actions to install package 'Twilio.AspNet.Mvc.5.0.1' Adding package 'JWT.1.3.4' to folder 'C:\_dev\repo\Intelliview_MyAccount\packages' Added package 'JWT.1.3.4' to folder 'C:\_dev\repo\Intelliview_MyAccount\packages' Added package 'JWT.1.3.4' to 'packages.config' Successfully installed 'JWT 1.3.4' to PbPresortServices Adding package 'Microsoft.AspNet.Razor.1.0.20105.408' to folder 'C:\_dev\repo\Intelliview_MyAccount\packages' Added package 'Microsoft.AspNet.Razor.1.0.20105.408' to folder 'C:\_dev\repo\Intelliview_MyAccount\packages' Added package 'Microsoft.AspNet.Razor.1.0.20105.408' to 'packages.config' Successfully installed 'Microsoft.AspNet.Razor 1.0.20105.408' to PbPresortServices Adding package 'Microsoft.AspNet.WebPages.1.0.20105.408' to folder 'C:\_dev\repo\Intelliview_MyAccount\packages' Added package 'Microsoft.AspNet.WebPages.1.0.20105.408' to folder 'C:\_dev\repo\Intelliview_MyAccount\packages' Added package 'Microsoft.AspNet.WebPages.1.0.20105.408' to 'packages.config' Successfully installed 'Microsoft.AspNet.WebPages 1.0.20105.408' to PbPresortServices Adding package 'Microsoft.AspNet.Mvc.3.0.20105.1' to folder 'C:\_dev\repo\Intelliview_MyAccount\packages' Added package 'Microsoft.AspNet.Mvc.3.0.20105.1' to folder 'C:\_dev\repo\Intelliview_MyAccount\packages' Added package 'Microsoft.AspNet.Mvc.3.0.20105.1' to 'packages.config' Successfully installed 'Microsoft.AspNet.Mvc 3.0.20105.1' to PbPresortServices Adding package 'Newtonsoft.Json.9.0.1' to folder 'C:\_dev\repo\Intelliview_MyAccount\packages' Added package 'Newtonsoft.Json.9.0.1' to folder 'C:\_dev\repo\Intelliview_MyAccount\packages' Added package 'Newtonsoft.Json.9.0.1' to 'packages.config' Executing script file 'C:\_dev\repo\Intelliview_MyAccount\packages\Newtonsoft.Json.9.0.1\tools\install.ps1'... Successfully installed 'Newtonsoft.Json 9.0.1' to PbPresortServices Adding package 'Twilio.5.0.0' to folder 'C:\_dev\repo\Intelliview_MyAccount\packages' Added package 'Twilio.5.0.0' to folder 'C:\_dev\repo\Intelliview_MyAccount\packages' Added package 'Twilio.5.0.0' to 'packages.config' Successfully installed 'Twilio 5.0.0' to PbPresortServices Install failed. Rolling back... Package 'Twilio.AspNet.Mvc.5.0.1 : Microsoft.AspNet.Mvc [3.0.20105.1, ), Twilio [5.0.0, )' does not exist in project 'PbPresortServices' Removed package 'Twilio.5.0.0 : JWT [1.3.4, ), Newtonsoft.Json [9.0.1, )' from 'packages.config' Removed package 'Newtonsoft.Json.9.0.1' from 'packages.config' Removed package 'Microsoft.AspNet.Mvc.3.0.20105.1 : Microsoft.AspNet.Razor [1.0.20105.408, ), Microsoft.AspNet.WebPages [1.0.20105.408, )' from 'packages.config' Removed package 'Microsoft.AspNet.WebPages.1.0.20105.408 : Microsoft.AspNet.Razor [1.0.20105.408, ), Microsoft.Web.Infrastructure [1.0.0, )' from 'packages.config' Removed package 'Microsoft.AspNet.Razor.1.0.20105.408' from 'packages.config' Removed package 'JWT.1.3.4' from 'packages.config' Package 'Twilio.AspNet.Mvc.5.0.1 : Microsoft.AspNet.Mvc [3.0.20105.1, ), Twilio [5.0.0, )' does not exist in folder 'C:\_dev\repo\Intelliview_MyAccount\packages' Removing package 'Twilio.5.0.0 : JWT [1.3.4, ), Newtonsoft.Json [9.0.1, )' from folder 'C:\_dev\repo\Intelliview_MyAccount\packages' Removed package 'Twilio.5.0.0 : JWT [1.3.4, ), Newtonsoft.Json [9.0.1, )' from folder 'C:\_dev\repo\Intelliview_MyAccount\packages' Removing package 'Newtonsoft.Json.9.0.1' from folder 'C:\_dev\repo\Intelliview_MyAccount\packages' Removed package 'Newtonsoft.Json.9.0.1' from folder 'C:\_dev\repo\Intelliview_MyAccount\packages' Removing package 'Microsoft.AspNet.Mvc.3.0.20105.1 : Microsoft.AspNet.Razor [1.0.20105.408, ), Microsoft.AspNet.WebPages [1.0.20105.408, )' from folder 'C:\_dev\repo\Intelliview_MyAccount\packages' Removed package 'Microsoft.AspNet.Mvc.3.0.20105.1 : Microsoft.AspNet.Razor [1.0.20105.408, ), Microsoft.AspNet.WebPages [1.0.20105.408, )' from folder 'C:\_dev\repo\Intelliview_MyAccount\packages' Removing package 'Microsoft.AspNet.WebPages.1.0.20105.408 : Microsoft.AspNet.Razor [1.0.20105.408, ), Microsoft.Web.Infrastructure [1.0.0, )' from folder 'C:\_dev\repo\Intelliview_MyAccount\packages' Removed package 'Microsoft.AspNet.WebPages.1.0.20105.408 : Microsoft.AspNet.Razor [1.0.20105.408, ), Microsoft.Web.Infrastructure [1.0.0, )' from folder 'C:\_dev\repo\Intelliview_MyAccount\packages' Removing package 'Microsoft.AspNet.Razor.1.0.20105.408' from folder 'C:\_dev\repo\Intelliview_MyAccount\packages' Removed package 'Microsoft.AspNet.Razor.1.0.20105.408' from folder 'C:\_dev\repo\Intelliview_MyAccount\packages' Removing package 'JWT.1.3.4' from folder 'C:\_dev\repo\Intelliview_MyAccount\packages' Removed package 'JWT.1.3.4' from folder 'C:\_dev\repo\Intelliview_MyAccount\packages' Could not install package 'Twilio.AspNet.Mvc 5.0.1'. You are trying to install this package into a project that targets '.NETFramework,Version=v4.0', but the package does not contain any assembly references or content files that are compatible with that framework. For more information, contact the package author. ========== Finished ==========

Move to GitHub Actions from AppVeyor

Currently, the builds are run on AppVeyor and the artifacts are uploaded there.
I suggest we use GitHub Actions so everyone can see how we build the project, and store the releases on GitHub alongside git tags.
When a release is created, the artifacts should be pushed to NuGet.

8.0.x breaking change

I've noticed breaking change with 7.0.0 -> 8.0.x related to Request Validation.
Previously there was code that was trimming off a trailing slash from a provided BaseUrlOverride. That is no longer happening. So when my Base URL is provided with a trailing slash, the validation fails and 403 errors when Twilio calls my services. I fixed my side by adding TrimEnd('/') myself, but I think this should be noted in the changes to hopefully save others time in the future. Thanks.

Request to upgrade System.Net.Http referenced from 4.3.0 to 4.3.4

Issue Summary

Running a SCA scan ( veracode) on twilio.aspnet.common 5.68.3 package reports the following vulnerabilities

Information Disclosure

System.Net.Http is vulnerable to an information disclosure. The library does not clear it's authentication headers during redirection, allowing a malicious user to use a redirect to gain access to information in the authentication header.
7.0
High
Data Source: Public Disclosure
Vulnerability ID: CVE-2018-8292

Cross-Origin Resource Sharing (CORS) Bypass

System.Net.Http is vulnerable to cross-origin resource sharing (CORS) bypass. An attacker is able to exploit the vulnerability to retrieve confidential user and system information.
7.0
High
Data Source: Public Disclosure
Vulnerability ID: CVE-2019-0545

Details

Affected Library: System.Net.Http, NUGET, system.net.http
Type: Transitive dependency
Version In Use: 4.​3.​0
Released On: 15 Nov 2016 00:00AM GMT

Suggested Fix

This issue was fixed in version 4.3.4 of System.Net.Http. That version is currently considered safe, we suggest that you upgrade to the fixed version.

Technical details:

twilio.aspnet.common version: 5.68.3
csharp version: net5.0

Request to upgrade referenced Microsoft.AspNetCore.Mvc.Core from 1.0.4 to 1.1.5

Issue Summary

Running a SCA scan ( veracode) on twilio.aspnet.common 5.68.3 package reports the following vulnerabilities

Information Disclosure

ASP.NET Core is affected by an information disclosure vulnerability. An attacker is able to bypass cross-origin resource sharing configurations to retrieve content from a web application.
7.0
High
Data Source: Public Disclosure
Vulnerability ID: CVE-2017-8700

Details

Affected Library: Microsoft.AspNetCore.Mvc.Core, NUGET, microsoft.aspnetcore.mvc.core
Type: Transitive dependency
Version In Use: 1.​0.​4
Released On: 9 May 2017 01:00AM GMT

Suggested Fix

This issue was fixed in version 1.0.6. However, that version is itself subject to other vulnerabilities, we suggest that you upgrade to 1.1.5, which is considered safe.

Technical details:

twilio.aspnet.common version: 5.68.3
csharp version: net5.0

Add TwiML extension methods

I'm not sure what the best practices are around extending controllers as libraries. If there are some good open-source repos that we could take a look at, that would be helpful.

I was wondering if, in addition to letting consumers inherit from TwilioController and using the inherited TwiML methods, if we could add extension methods to the ControllerBase class.
The benefit of this would be that consumers who already have their controller inherit from a different class, can also use the same TwiML method, without having to switch base class.

Looking for feedback on this one!

.net core SmsRequest FormData empty

Hi,

First time using Twilio in a while, and trying to use .net core 5 with Twilio.Aspnet.Core is giving me some trouble. I am testing locally using ngrok and for whatever reason the form data passed is empty. This is causing my SmsRequest object to have null properties.

Any ideas? Could it be something with ngrok?

image

Add extension methods to `VoiceResponse` and `MessagingResponse` to convert to `TwiMLResult`

We could add extension methods to make the TwiML generation code more eloquent.
Consider this code:

app.MapPost("/submit", () =>
{
    var voiceResponse = new VoiceResponse()
        .Say($"You pressed 1. Good choice. Goodbye.");
    return Results.Extensions.TwiML(voiceResponse);
});

Which could become:

app.MapPost("/submit",  () => 
    new VoiceResponse()
        .Say($"You pressed 1. Good choice. Goodbye.")
        .ToTwiMLResult()
);

with a convenient extension method.

Latest version (8.0) Not compatible with Azure Function Apps v4

Thanks to how the function app runtime is setup, and the use of .net 7 logging packages in twilio-asp.net v8.0, it is no longer compatible with the azure function v4 runtime.

repro steps:

  1. create a new function app v4 in visual studio
  2. install twilio and twilio.aspnet.core nuget packages
  3. run function app, note runtime error linking logging extensions v7.

Microsoft is aware of the issue and did the same thing with their entity framework (oops!). more info here:
Azure/Azure-Functions#2319

version 7 of this package is flagged with critical errors. any chance you could package up another version of 8.0 that uses the .net 6 logging framework? (2.1/2.2)?

Thanks!

Request validation incorrectly returns 403

In my ASP.NET Core application I updated the Twilio.AspNet.Common package to the latest version 6.0.0 and I wanted to start using the new ValidateRequestAttribute for request validation.

I added the [ValidateRequest] (and [AllowAnonymous]) attribute on the controller level, so all actions should behave the same with regards to request validation.

But interestingly, for some requests from Twilio the validation filter returns a 403 while others go through just fine.

I noticed one difference between failing and succeeding requests: All the failing request URLs contain custom query parameters, for example:
https://xxx.xxx/api/Twilio/CallerIntroduction?applicationUserId=94c08247-320a-4e53-b3f2-8c175feb588e&iteration=0

The succeeding requests are plain urls without query parameters, for example:
https://xxx.xxx/api/Twilio/DisclaimerAndWait

Does the ValidateRequestAttribute from the Twilio.AspNet.Common 6.0.0 package have a bug with regards to handling of query parameters, or am I doing something wrong?

Change default for `AllowLocal` Twilio request validation to `false`.

When AllowLocal is not configured or set to null, the request validation filters and middleware will default the behavior to true, allowing local HTTP requests.
This has been raised as a vulnerability internally at Twilio. While this feature is helpful for testing your APIs locally, it is vulnerable to Server-Side Request Forgery.
While this is not an officially supported library, contributors of this library will remediate this vulnerability soon. AllowLocal will default to false in the next major release.

Breaking changes moving from to 5.73.0 -> 5.77.0

Hi-

I upgraded to 5.77.0 from 5.73.0, and it seems to be breaking other libraries or at least modifying something that doesn't work with other libraries. Not sure what changed in that release that would affect other libs.

This update breaks Apple Sign In using OAuth 2 using the AspNet.Security.OAuth.Apple nuget package. For whatever reason it seems to be hijacking the http client when making a token request and Apple is returning "invalid_client". It's easy to reproduce by using 5.77.0 and integrating apple authorization.

Add `Enabled` option for request validation, in favor of `AllowLocal` (please provide feedback)

Recently, the default value for AllowLocal has been changed from true to false.
This was because AllowLocal makes the request validation vulnerable to Server-Side Request Forgery.

Maybe it makes more sense to build in a kill-switch to turn on/off request validation as a whole, instead of AllowLocal.
This option would respect .NET configuration's reloadOnChange feature, so it can be changed without having to restart the application.

I'm just thinking out loud here and would like feedback, thank you!

TwiMLResult doesn't work with custom encoding

I try to use the programmable voice for Swedish language and the callback fails with exception.
It is happens because the server locale is en-us and Encoding.Default returns ASCII. I need a UTF8 for my case.
Default encoding is hard coded in Twilio.AspNet.Mvc.TwiMLResult.LoadFromString.

My proposal is to add an overloaded version with encoding parameter.

The current workaround is to subclass from the TwiMLResult and add the missing method.

Extend SmsRequest with collection of media links for MMS webhook

Would be great to extend SmsRequest class wrapping the Twilio POST webhook request for incoming MMS with the property bound to media files collection. Now to retrieve the media files from Twilio webhook call on ASP.NET Core API (while I am getting incoming MMS from external phone number) I have to read from Request.Form to retrieve media related parameters.

    [HttpPost("message")]
    [AllowAnonymous]
    public async Task<TwiMLResult> MessageWebhook(SmsRequest request, CancellationToken token) {
        ...
        for (int i = 0; i < request.NumMedia; i++)
        {
            string? mediaUrl = Request.Form[$"MediaUrl{i}"];
            string? contentType = Request.Form[$"MediaContentType{i}"];
            ...
        }
    }

Would be great to just mark SmsRequest request as [FromForm] SmsRequest request and access media related items like this:

List<MediaFile> mediaFiles = request.MediaFiles;

So ASP.NET Core controller for handling SMS/MMS webhooks woul dlook like this:

using Twilio.AspNet.Common;
using Twilio.AspNet.Core; // or .Mvc for .NET Framework
using Twilio.TwiML;

public class WebhookController : TwilioController
{
    [HttpPost("message")]
    [AllowAnonymous]
    public async Task<TwiMLResult> MessageWebhook([FromForm] SmsRequest request, CancellationToken token) {
        ...
        foreach (var media in request.MediaFiles)
        {
            string mediaUrl = media.Url;
            string contentType = media.ContentType;
         }
        ... 
    }
}

No apparent way to update username/password for injected TwilioRestClients

After reading this blog post about the new startup extension, I was looking forwared to injecting TwilioRestClients instead of using the static TwilioClient.Init(accountSid, authToken); approach.

But there doesn't seem to be a way to change the username & password for the injected client, like TwilioClient.SetUsername/SetPassword. For scenarios such as sending texts to different subaccounts with different accountSid & authToken from the same API, I'm not sure that I can set this dynamically per-request.

If there is a way to do this without resorting to the previous static approach, it might be good to update the docs. Thanks!

RequestValidationHelper Ignores allowLocal

The Twilio.AspNet.Core.RequestValidationHelper class ignores the allowLocal parameter on one of the overloads.

Currently:

public bool IsValidRequest(HttpContext context, string authToken, bool allowLocal = true)
{
    return IsValidRequest(context, authToken, null);
}

Should be:

public bool IsValidRequest(HttpContext context, string authToken, bool allowLocal = true)
{
    return IsValidRequest(context, authToken, null, allowLocal);
}

How to nest commands with this library?

Hello, I've been searching on how to nest commands using this library, but there is not a lot of information about this library and asp.net core; There is about the other SDKs on twilios site, but the commands on this one and the logic seem different

How can I achieve a TwiML result like this one using this library:

<Response> <Gather method="POST" timeout="15" numDigits="4"> <Say voice="alice" language="en-US">Input a code</Say> </Gather> </Response>

Transient System.Security.Cryptography.CryptographicException in TwilioRequestValidator

We have an issue where the TwilioRequestValidator is throwing an exception. The servers will work fine for weeks, and then the incoming requests will begin failing suddenly, until a server restart is done.

I've opened a stackoverflow post on this item, but some insight from Twilio would be helpful.

https://stackoverflow.com/questions/54699303/transient-system-security-cryptography-cryptographicexception-in-twiliorequestva

Include port when validating an http request

Based on the docs

For SMS and voice callbacks over HTTP, Twilio will drop the username and password (if any) from the URL before computing the signature. For SMS and voice callbacks over HTTPS, Twilio will drop the username, password and port (if any) before computing the signature.

it seems that the port has to be included in the hash when it is an HTTP request, if this is indeed the case this has to be change to something like

var requestedUrl = $"{request.Scheme}://{(request.IsHttps ? request.Host.Host : request.Host.ToUriComponent())}{request.Path}{request.QueryString}";

"Identity not provided" when try to add an SMS participant

Adding an SMS participant does not need to provide an Identifier, and the GetParam() method returns a dictionary that contains " and "MessagingBinding.ProxyAddress". But the endpoint needs the null Identifier property.
So In the GetParams() method on CreateParticipantOptions class we should remove the condition that adds the Identifier row to the dictionary if the identifier is not null and let the identifier be added to the dictionary anytime.

Queue Parameters missing from VoiceRequest

None of the request classes have parameters such as QueueTime, QueueSid, QueueResult, QueuePosition, CurrentQueueSize, AvgQueueTimeOn which are used in callbacks from the waitUrl and action for the Enqueue verb. The current workaround would be to create a class myself that inherits from VoiceRequest and include these, but it seems like they should be included in VoiceRequest class itself.

Net framework quickstart: responding to SMS, incoming message is empty

I have followed the example to receive a response from an SMS using ASP.Net

I have ngrok installed and working.
My Console has been updated with URL from ngrok with \sms appended
My asp app is running.
I can debug code and get breakpoint in my SMSController class.
But the incomingMessage.Body is Nothing

Using vb.net in my ASP app.

local
HomeController code
SMSController code
Create SMS Code
twilio Console
Ngrok

Error 500 cannot load API for Swagger

Visual Studio 2022 ASP.NET Core Web API. When using 5.71 Swagger has an error 500 and will not load swagger.json.
The error received is: Actions require an explicit HttpMethod binding for Swagger/OpenAPI 3.0
I am unsure how to fix this but if I revert back to 5.68.3 it works fine. I am unsure what else you may need to know.
Currently the API is essentially like the one out of the box in Visual Studio.

Add guidance to how to bind Twilio Webhook XML to objects in Minimal APIs

In MVC, XML would be automatically bound to objects which made it easy to accept the data as strongly typed object when receiving Twilio webhook XML.
Minimal APIs does not do this, so we need to figure out what the best practice would be to do this for Minimal APIs, and if necessary add features to this library to make it easier.

Ideally, you would be able to accept the webhook payload like this:

app.MapPost("/sms", (SmsRequest smsRequest) =>
{
    var messagingResponse = new MessagingResponse();
    messagingResponse.Message($"You said: {smsRequest.Body}");
    return Results.Extensions.TwiML(messagingResponse);
});

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.