Click here to Skip to main content
15,881,852 members
Articles / Programming Languages / C#

ApiFrame : A simple library for Web API security, exception and versioning

Rate me:
Please Sign up or sign in to vote.
4.78/5 (22 votes)
3 May 2014CPOL10 min read 64.3K   227   51   22
A simple C#.NET library that implements HMAC Authentication, Global Exception Handling and API Versioning

ApiFrame

Related article

How to integrate ApiFrame in ASP.NET Web API Application[^]

Contents

Introduction

ApiFrame is a simple .NET library that implements the core components required for ASP.NET WEB API development. It's a C# class library with logical separation of Server (Provider), Client (Consumer) and the supporting Framework (An Infrastructure). The Server components provide an interface to implement security (Authentication & Authorization), exception handling and versioning of Web API methods. The security mechanism implemented in this library is HMAC (Hash Message Authentication Code) authentication. One example of HMAC usage is Amazon web service (AWS) and the authentication process in this library uses a HMAC-SHA signature which is the same approach as AWS. The Client part provides a Gateway component that can be referenced by the .NET clients to consume the WEB API.

This article will serve as a simple documentation for ApiFrame that focus on providing a basic description about the class components within the library. The diagram below shows the outline of component architecture.

Component Architecture

HMAC Authentication – An Overview

HMAC authentication provides a simple way to authenticate a HTTP request using a secret key that is known to client and server. Both client and server have access to secret key. Generally, this secret key is a Unique Id which is created at the time of registration and stored in the database. Using the secret key and a message based on request content client generates a signature (MAC) using HMAC algorithm and this signature is attached to the authorization header of HTTP request. When the server receives the request, it extracts the hashed signature (MAC) from the request header and calculates its own version of signature to verify if the received signature matches the calculated signature. If the two signature matches, then the system concludes that the request is valid and should be served. If the two signatures don’t match, then the request is dropped and the system responds with an error message.

Image 3

Framework

The Framework provides token models, configuration class, constant values, helpers, extensions to support the implementation of server and client components. Also, it offers an Interface to integrate with the other applications through dependency injection.

Tokens

HMAC authentication depends on token based communication. A Token is a small piece of data that is attached to the authorization header of every API request. In general, Tokens are keys that are used for authentication and authorization of the HTTP request. In the below class diagram, you will notice that Base class "ApiBaseToken" has the following properties

  • AccessToken: Access token is a Public key
  • SecretToken: Secret Token is a private key used for generating the signature using a hash algorithm
  • AuthScheme: AuthScheme is a simple abbreviated text that indicates the consumer (application / customer) name

There are three classes derived from this Base class, each represents a token model used for a distinct purpose. ApiApplicationToken is required for Authentication. ApiUserToken is required for Authorization. ApiRequestToken is required for creating a client request.

Image 4

Interface

There are few areas in the library where dependencies need to be injected. To achieve this, the required interfaces are defined. The using application should supply the required tokens to authenticate and authorize an HTTP request. The interface IApiInception expose the necessary methods that the calling assembly need to implement which returns the tokens required to process the HTTP Request. The interface IApiException is for exception logging. Every application has its own exception logging mechanism. When an unhandled program exception is thrown in the using application, It can be caught by implementing the IApiException interface. The interface IApiSignature is to calculate HMAC signature. The library itself contains an implementation of this interface that uses HMACSHA256 algorithm to calculate the HMAC signature. The same implementation can be used or a different implementation can be injected using this interface if required.

Image 5

C#
public interface IApiInception
{
    ApiApplicationToken GetApplicationToken(string accessToken);
    ApiUserToken GetUserToken(string username, string password);
    ApiUserToken GetUserToken(string accessToken);
}

public interface IApiException
{
    void LogMessage(Exception exception);
}

public interface IApiSignature
{
    string CalculateHmac(string secretKey, string stringToSign);
}

Dependency Injection

A simple Dependency Injection class is defined with two generic static methods that allows to inject objects into a class. RegisterType<TInterface, TClass> method is called by the using application to map an interface type with corresponding concrete class type. GetInstance<TInterface> method is called by the internal class components to get the object instance of the registered interface type.

Image 6

C#
public class ApiObjectFactory
{
    private static readonly Dictionary<string, System.Type> ObjectTypes
         = new Dictionary<string, System.Type>();

    public static void RegisterType<TInterface, TClass>() where TClass : TInterface
    {
        var typeInterface = typeof(TInterface);
        var typeClass = typeof(TClass);

        if (!typeInterface.IsInterface || typeClass.IsInterface || typeClass.IsAbstract)
        {
            throw new ApiRequestException(ApiErrorCode.InvalidOperation);
        }

        ObjectTypes.Add(typeInterface.Name, typeClass);
    }

    public static TInterface GetInstance<TInterface>()
    {
        System.Type type = null;

        if (ObjectTypes.TryGetValue(typeof(TInterface).Name, out type))
        {
            return (TInterface)System.Activator.CreateInstance(type);
        }

        throw new ApiRequestException(ApiErrorCode.MissingRequiredType);
    }
}

Signature Calculation

When implementing HMAC authentication, Every API request requires signing with HMAC signature. The signature is computed with the token “SecretKey” and a message “StringToSign”. The “StringToSign” is constructed using the URI, request timestamp and other HTTP header values. The computed signature is converted to base64 string. This encoded base64 string is the calculated signature used for signing the HTTP request.

Image 7

C#
public string CalculateHmac(string secretKey, string stringToSign)
    {
        byte[] secretBytes = Encoding.UTF8.GetBytes(secretKey);
        byte[] stringBytes = Encoding.UTF8.GetBytes(stringToSign);

        string signature;

        using (var hmac = new HMACSHA256(secretBytes))
        {
            byte[] hash = hmac.ComputeHash(stringBytes);
            signature = Convert.ToBase64String(hash);
        }

        return signature;
    }

Configuration

A Configuration class "ApiConfiguration" implements a singleton instance with properties shown in the below class diagram. "RequestValidityInMinutes" property is used to configure the request validity in minutes, that no HTTP Request can be older than x minutes. The x value is configured through the instance of ApiConfiguration class. Its initial value defaults to 10 minutes. This can be modified by the using application. "VersionNamespaceKey" property is used to configure the Web API versioning method. ApiFrame allows two different method to implement versioning. It can be configured using "VersionNamespaceKey" property and it defaults to "area"

Image 8

C#
public class ApiConfiguration
{
    static ApiConfiguration()
    {
        Instance = new ApiConfiguration();
        Instance.RequestValidityInMinutes = 10;
        Instance.VersionNamespaceKey = "area";
    }

    public static ApiConfiguration Instance { get; private set; }
    public double RequestValidityInMinutes { get; set; }
    public string VersionNamespaceKey { get; set; }
}

Utilities

This part of the library includes utility classes such as helpers, extensions and validation methods to access and validate the HTTP request and headers.

Image 9

Server

The server components implements the required filters for Authentication and Authorization. The filters verifies the HTTP request and identifies the requester. Verifying a HTTP request on the server side involves three steps which is shown in the below diagram. The first step is to retrieve the secret token using the access token. The second step is to calculate the signature from the request parameters and secret token. The third step is to verify if the calculated signature matches the received signature.

Image 10

Security

The class diagram below shows the design of the server component that handles the Authentication and Authorization.

Image 11

Authentication

Authentication is identifying the user based on username and password. ApiFrame implements an authorization filter attribute “ApiAuthentication” that follows the HMAC method such as validating the signature to authenticate a user. It reads the username and password from HTTP request header and uses it to identify the user. If the user is valid, then it creates a custom identity and principal object. This custom principal object is set to the user property of current HttpContext.

C#
public class ApiAuthentication : AuthorizationFilterAttribute
{
    public override void OnAuthorization(HttpActionContext actionContext)
    {
        // Get the request for the action context
        HttpRequestMessage request = actionContext.Request;

        // Create an instance of ApiValidation
        IApiValidation apiValidation = new ApiValidation();

        // Call the validation method to validate the request
        apiValidation.ValidateRequest(request);

        // Get the access token from the request header parameter and validate
        string apiAccessToken = request.GetAccessToken();

        // Get the instance of ApiInception
        IApiInception apiInception = ApiObjectFactory.GetInstance<IApiInception>();

        // Get the application token
        ApiApplicationToken applicationToken = apiInception.GetApplicationToken(apiAccessToken);

        // Call the validation method to validate the token and signature
        apiValidation.ValidateToken(request, applicationToken);
        apiValidation.ValidateSignature(request, applicationToken.SecretToken);

        // Read the username and password from current http request paramters
        var username = HttpContext.Current.Request.Params["Username"];
        var password = HttpContext.Current.Request.Params["Password"];

        if (username == null || password == null)
        {
            throw new ApiRequestException(ApiErrorCode.MissingRequiredParamter);
        }

        // Call the service method to get the user details
        ApiUserToken user = apiInception.GetUserToken(username, password);
        bool isAuthenticated = user != null;

        if (isAuthenticated)
        {
            // Set the user principal for the current http request
            string authenticationType = ApiConstants.AUTHTYPE;
            IIdentity userIdentity = new ApiIdentity(authenticationType, isAuthenticated, user.Name, user.UserId);
            IPrincipal principal = new ApiPrincipal(userIdentity, user.Roles);
            HttpContext.Current.User = principal;
        }
        else
        {
            throw new ApiRequestException(ApiErrorCode.AuthenticationFailed);
        }
    }
}

Authorization

Authorization is for checking whether the authenticated user is allowed to perform an action or access a protected resource. In ApiFrame, for authorization we have an authorization attribute class named “ApiAuthorization” derived from “AuthorizeAttribute” that validates the signature and identifies the authenticated user using the User Token in the HTTP request.

C#
public class ApiAuthorization : AuthorizeAttribute
{
    public override void OnAuthorization(HttpActionContext actionContext)
    {
        HttpRequestMessage request = actionContext.Request;

        // Get the instnce of ApiValidation
        IApiValidation apiValidation = new ApiValidation();

        // Call the validation method to validate the request
        apiValidation.ValidateRequest(request);

        string accessToken = request.GetAccessToken();

        // Get the instance of ApiInception
        IApiInception apiInception = ApiObjectFactory.GetInstance<IApiInception>();
        ApiUserToken user = apiInception.GetUserToken(accessToken);

        apiValidation.ValidateToken(request, user);
        apiValidation.ValidateSignature(request, user.SecretToken);

        if (!string.IsNullOrEmpty(Roles))
        {
            apiValidation.ValidateRole(Roles, user.Roles);
        }

        string authenticationType = ApiConstants.AUTHTYPE;
        IIdentity userIdentity = new ApiIdentity(authenticationType, true, user.Name, user.UserId);
        IPrincipal principal = new ApiPrincipal(userIdentity, user.Roles);
        HttpContext.Current.User = principal;
    }
}

Authorized Request

If the using application doesn't require authentication and authorization for public API methods but want to allow access to athorized clients then In that case, an authorization filter attribute "ApiAuthorizedRequest" is defined. This can be applied to action methods to checks if the request is from an authorized client.

C#
public class ApiAuthorizedRequest : AuthorizationFilterAttribute
{
    public override void OnAuthorization(HttpActionContext actionContext)
    {
        HttpRequestMessage request = actionContext.Request;

        // Get the instnce of ApiValidation
        IApiValidation apiValidation = new ApiValidation();

        // Call the validation method to validate the request
        apiValidation.ValidateRequest(request);

        string apiAccessToken = request.GetAccessToken();

        IApiInception apiInception = ApiObjectFactory.GetInstance<IApiInception>();

        // Get the application token
        ApiApplicationToken applicationToken = apiInception.GetApplicationToken(apiAccessToken);

        apiValidation.ValidateToken(request, applicationToken);
        apiValidation.ValidateSignature(request, applicationToken.SecretToken);
    }
}

Custom Identity and Principal

When the server authenticates / authorizes the user, it creates a custom principal (ApiPrincipal) which is an IPrincipal object that represents the security context under which code is running. The principal contains an associated custom identity object (ApiIdentity) that contains information about the user. This security information (ApiPrincipal) is set for the current HTTP request (HttpContext.Current.User) when authenticated / authorized. The below code shows the custom Identity and Principal class

C#
public class ApiIdentity : IIdentity
{
    public ApiIdentity(string authenticationType, bool isAuthenticated, string userName, string userId)
    {
        this.AuthenticationType = authenticationType;
        this.IsAuthenticated = isAuthenticated;
        this.Name = userName;
        this.UserId = userId;
    }

    public string AuthenticationType { get; private set; }
    public bool IsAuthenticated { get; private set; }
    public string Name { get; private set; }
    public string UserId { get; private set; }
}

public class ApiPrincipal : IPrincipal
{
    public ApiPrincipal(IIdentity identity, string roles)
    {
        this.Identity = identity;
        this.Roles = roles.Split(',');
    }

    public IIdentity Identity { get; private set; }
    public string[] Roles { get; private set; }

    public bool IsInRole(string roles)
    {
        return Roles.Intersect(roles.Split(',')).Count() > 0;
    }
}

Enforcing HTTPS

The communication through plain HTTP is not secure though we have secure authentication schemes. Enabling SSL is another layer of protection to data. We may need HTTPS for access to some protected resources. ApiFrame implements an authorization filter named “ApiHttpsRequired” that checks for SSL. This can be used for Web API methods that require HTTPS.

C#
public class ApiHttpsRequired : AuthorizationFilterAttribute
{
    public override void OnAuthorization(HttpActionContext actionContext)
    {
        if (actionContext.Request.RequestUri.Scheme != Uri.UriSchemeHttps)
        {
            throw new ApiRequestException(ApiErrorCode.InvalidUriScheme);
        }
        else
        {
            base.OnAuthorization(actionContext);
        }
    }
}

Exception

Image 12

Error Code

A set of error codes are defined in a enum type called ApiErrorCode that contains the list of error types handled by the ApiFrame.

Error Response

Error response are created by ApiErrorResponse class that contains a static method "GetErrorMessage" which returns a HttpResponseMessage for a given ApiErrorCode. The method identifies the custom error with the error code and creates a HttpError with the custom message, type and appropriate HttpStatusCode. This HttpError is serialized and assigned to the content of HttpResponseMesssage. ApiErrorResponse class provides another method "GetHttpError" to deserialize the content of HttpResponseMessage to HttpError.

C#
public class ApiErrorResponse
{
    private const string Code = "Code";
    private const string Type = "Type";

    public static HttpResponseMessage GetErrorMessage(ApiErrorCode errorCode)
    {
        HttpError error;

        switch (errorCode)
        {
            case ApiErrorCode.InvalidRequestHeader:
            case ApiErrorCode.InvalidMD5:
            case ApiErrorCode.InvalidSignature:
                error = new HttpError("Problem communicating with the application. Invalid Request.");
                error[Code] = HttpStatusCode.ExpectationFailed;
                break;
            case ApiErrorCode.InvalidTimestamp:
                error = new HttpError("The date and time is incorrect.");
                error[Code] = HttpStatusCode.Forbidden;
                break;
            case ApiErrorCode.InvalidScheme:
                error = new HttpError("This version of application is outdated.");
                error[Code] = HttpStatusCode.BadRequest;
                break;
            case ApiErrorCode.InvalidUriScheme:
                error = new HttpError("There has been problem processing your request.");
                error[Code] = HttpStatusCode.Forbidden;
                break;
            case ApiErrorCode.AuthenticationFailed:
                error = new HttpError("The username/passowrd you have entered is incorrect");
                error[Code] = HttpStatusCode.Forbidden;
                break;
            case ApiErrorCode.InvalidToken:
            case ApiErrorCode.InvalidRole:
                error = new HttpError("Authorization has been denied for this request");
                error[Code] = HttpStatusCode.Unauthorized;
                break;
            case ApiErrorCode.MissingRequiredParamter:
                error = new HttpError("The username/passowrd parameter is missing");
                error[Code] = HttpStatusCode.Forbidden;
                break;
            case ApiErrorCode.MissingRequiredType:
                error = new HttpError("The required type is not registered");
                error[Code] = HttpStatusCode.Forbidden;
                break;
            default:
                error = new HttpError("Server error.");
                error[Code] = HttpStatusCode.InternalServerError;
                break;
        }

        error[Type] = errorCode.ToString();

        var response = new HttpResponseMessage((HttpStatusCode)error[Code])
        {
            Content = new StringContent(new JavaScriptSerializer().Serialize(error)),
            ReasonPhrase = errorCode.ToString()
        };

        return response;
    }

    public static HttpError GetHttpError(HttpResponseMessage response)
    {
        if (!response.IsSuccessStatusCode)
        {
            string responseError = response.Content.ReadAsStringAsync().Result;
            var httpError = new JavaScriptSerializer().Deserialize<HttpError>(responseError);
            return httpError;
        }

        return null;
    }
}

Custom Http Request Exception

A class "ApiRequestException" is derived from HttpResponseException that provides a way to define the custom exception. The ApiRequestException class offers two constructors. One is to define the internal exceptions of ApiFrame and the other constructor allows the calling assembly to initialize its own custom exception.

C#
public class ApiRequestException : HttpRequestException
{
    public ApiRequestException(ApiErrorCode errorType)
    {
        this.ErrorType = errorType;
        this.ErrorResponseMessage = ApiErrorResponse.GetErrorMessage(errorType);
    }

    public ApiRequestException(HttpStatusCode statusCode, string errorMessage, string reasonPhrase)
    {
        this.ErrorType = ApiErrorCode.InvalidOperation;
        this.ErrorResponseMessage = new HttpResponseMessage(statusCode)
        {
            Content = new StringContent(errorMessage),
            ReasonPhrase = reasonPhrase
        };
    }

    public ApiErrorCode ErrorType { get; set; }
    public HttpResponseMessage ErrorResponseMessage { get; set; }
}

Exception Filter attribute

Web API exceptions can be handled by an exception filter. ApiExceptionAttribute is an exception filter derived from ExceptionFilterAttribute class and overrides the OnException method.

C#
public class ApiExceptionAttribute : ExceptionFilterAttribute
{
    public override void OnException(HttpActionExecutedContext context)
    {
        HttpResponseMessage response;

        if (context.Exception is ApiRequestException)
        {
            ApiRequestException apiException = (ApiRequestException)context.Exception;
            response = apiException.ErrorResponseMessage;
        }
        else
        {
            ApiObjectFactory.GetInstance<IApiException>().
                LogMessage(context.Exception);

            response = ApiErrorResponse.GetErrorMessage(ApiErrorCode.InvalidProgramException);
        }

        context.Response = response;
    }
}

Versioning

A custom class “ApiControllerSelector” is defined that implement IHttpControllerSelector to support versioning. ApiFrame provides an option to version Web APIs using Namespaces or Areas. The code is taken from this MSDN blog[^] and is slightly tweaked to work with MVC Areas.

Image 13

Controller Selector

The interface that Web API uses to select a controller is IHttpControllerSelector. The method on this interface is SelectController, which selects a controller for an HttpRequestMessage. A custom class “ApiControllerSelector” is defined that implement IHttpControllerSelector to support versioning.

C#
public HttpControllerDescriptor SelectController(HttpRequestMessage request)
    {
        IHttpRouteData routeData = request.GetRouteData();
        if (routeData == null)
        {
            throw new HttpResponseException(HttpStatusCode.NotFound);
        }

        // Get the namespace and controller variables from the route data.
        string namespaceName = GetRouteVariable<string>(routeData, ApiConfiguration.Instance.VersionNamespaceKey);
        if (namespaceName == null)
        {
            throw new HttpResponseException(HttpStatusCode.NotFound);
        }

        string controllerName = GetRouteVariable<string>(routeData, ControllerKey);
        if (controllerName == null)
        {
            throw new HttpResponseException(HttpStatusCode.NotFound);
        }

        // Find a matching controller.
        // string key = String.Format(CultureInfo.InvariantCulture, "{0}.{1}", namespaceName, controllerName);
        string key = ApiConfiguration.Instance.VersionNamespaceKey == NamespaceKey ?
            String.Format(CultureInfo.InvariantCulture, "{0}.Controllers.{1}", namespaceName, controllerName) :
            String.Format(CultureInfo.InvariantCulture, "{0}.{1}", namespaceName, controllerName);

        HttpControllerDescriptor controllerDescriptor;
        if (controllers.Value.TryGetValue(key, out controllerDescriptor))
        {
            return controllerDescriptor;
        }
        else if (duplicates.Contains(key))
        {
            throw new HttpResponseException(
                request.CreateErrorResponse(HttpStatusCode.InternalServerError,
                "Multiple controllers were found that match this request."));
        }
        else
        {
            throw new HttpResponseException(HttpStatusCode.NotFound);
        }
    }

Client

The client part implements a Gateway component which can be used by API clients to consume the WEB API methods. Below shown is the class diagram of the Client. The ApiRequestToken contains the required properties to create an Http request which is passed as an input parameter to the execute method of Gateway class component. The primary job of the Gateway is to send the HTTP request to the server and receive the response.

Image 14

Gateway

Gateway component is responsible for creating a signed HTTP request and sending it to server.Sending a signed HTTP request involves three steps as shown in the below diagram. The first step is to construct a HTTP request with all the required request header parameters. The second step is to create a HMAC-SHA signature using the secret token and “StringToSign” message based on request content. The third step is to send the request and the signature to the server.

Image 15

C#
private HttpResponseMessage SendHttpRequest(ApiRequestToken requestToken)
{
    DateTime requestDate = ApiHelper.GetCurrentDateTime();
    string contentType = string.Empty;
    string contentMD5 = string.Empty;

    if (!string.IsNullOrEmpty(requestToken.Content))
    {
        contentType = ApiConstants.CONTENTTYPE;
        contentMD5 = ApiHelper.ComputeMD5Hash(requestToken.Content);
    }

    // Step 1: Create the http request
    HttpRequestMessage request = new HttpRequestMessage(requestToken.Verb, requestToken.RelativeUrl);
    request.Headers.Date = requestDate;

    if (!string.IsNullOrEmpty(requestToken.Content))
    {
        request.Content = new StringContent(requestToken.Content);
        request.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType);
        request.Content.Headers.Add("Content-MD5", contentMD5);
    }

    // Step 2: Sign the request
    string stringToSign = ApiHelper.BuildMessageRepresentation(requestToken.Verb.ToString(), contentType, contentMD5, requestDate, ApiConstants.FORWARDSLASH + requestToken.RelativeUrl);
    string signature = this.signature.CalculateHmac(requestToken.SecretToken, stringToSign);
    string authorizationHeader = ApiHelper.BuildAuthorizationHeader(requestToken.AuthScheme, requestToken.AccessToken, signature);
    request.Headers.Add("Authorization", authorizationHeader);

    // Step 3: Send the request
    using (var client = new HttpClient())
    {
        client.BaseAddress = new Uri(this.baseUrl);
        client.DefaultRequestHeaders.Accept.Clear();
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(ApiConstants.APPJSON));
        ServicePointManager.ServerCertificateValidationCallback += (sender, certificate, chain, sslPolicyErrors) => true;
        HttpResponseMessage response = client.SendAsync(request).Result;
        return response;
    }
}

“Message” based on request content (StringToSign)

StringToSign is a message that is constructed using the elements of a HTTP request. Following is the sample code that illustrates how the StringToSign is constructed. Content-MD5 and Content-Type will be available only for HTTP POST request. For other http request such as GET, PUT, DELETE, the Content value is simply represented as empty.

C#
StringToSign = HTTP-VERB + "\n" +
Content-MD5 + "\n" +
Content-Type + "\n" +
TimeStamp + "\n" +
RequestUri;

Signing the Request

Signing the request is adding the HMAC signature to the authorization header of the HTTP request in the below given form:

C#
Authorization: AuthScheme AccessToken:Signature

Conclusion

The article described the parts of the library and what it contains. A related article posted on How to integrate ApiFrame in ASP.NET Web API Application[^] that provides the guidelines to use APIFrame.

References

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Ireland Ireland
Many years of experience in software design, development and architecture. Skilled in Microsoft .Net technology, Cloud computing, Solution Design, Software Architecture, Enterprise integration, Service Oriented and Microservices based Application Development. Currently, focusing on .Net Core, Web API, Microservices, Azure

Comments and Discussions

 
QuestionSource Code?? Pin
KristaDev1-Mar-17 13:11
KristaDev1-Mar-17 13:11 
QuestionNice Article --> Could you please share the source code Pin
sasikumard19781-Dec-16 19:31
sasikumard19781-Dec-16 19:31 
GeneralI can't download the source code Pin
Member 116051807-Aug-15 5:35
Member 116051807-Aug-15 5:35 
QuestionI need code, Can you please provide me code Pin
dipaklchaudhari1-Feb-15 22:06
dipaklchaudhari1-Feb-15 22:06 
Questioni need source Pin
dannychou7530-Oct-14 19:51
dannychou7530-Oct-14 19:51 
QuestionI need source! but can not download!! Pin
xingxing7128-Oct-14 12:26
xingxing7128-Oct-14 12:26 
QuestionCan not download the source-code Pin
lucasthehacker29-Sep-14 1:32
lucasthehacker29-Sep-14 1:32 
Questionhow to call in Javascript ? Pin
Yulianghua24-Jun-14 22:18
Yulianghua24-Jun-14 22:18 
Questionsource code file link broken Pin
Tridip Bhattacharjee13-May-14 21:22
professionalTridip Bhattacharjee13-May-14 21:22 
AnswerRe: source code file link broken Pin
John-ph13-May-14 21:53
John-ph13-May-14 21:53 
GeneralRe: source code file link broken Pin
Mehul.8923-Sep-14 4:05
Mehul.8923-Sep-14 4:05 
GeneralRe: source code file link broken Pin
John-ph23-Sep-14 4:21
John-ph23-Sep-14 4:21 
GeneralRe: source code file link broken Pin
Dale Janssen9-Oct-16 3:34
professionalDale Janssen9-Oct-16 3:34 
GeneralMy vote of 5 Pin
Agent__0075-May-14 19:32
professionalAgent__0075-May-14 19:32 
GeneralRe: My vote of 5 Pin
John-ph14-May-14 3:50
John-ph14-May-14 3:50 
GeneralMy vote of 5 Pin
Volynsky Alex2-May-14 20:28
professionalVolynsky Alex2-May-14 20:28 
Nice
GeneralRe: My vote of 5 Pin
John-ph3-May-14 22:07
John-ph3-May-14 22:07 
GeneralRe: My vote of 5 Pin
Volynsky Alex4-May-14 21:08
professionalVolynsky Alex4-May-14 21:08 
QuestionMy Vote of 5 Pin
mick_lennon1-May-14 23:10
mick_lennon1-May-14 23:10 
AnswerRe: My Vote of 5 Pin
John-ph3-May-14 22:07
John-ph3-May-14 22:07 
GeneralMy vote of 5 Pin
Humayun Kabir Mamun30-Apr-14 20:16
Humayun Kabir Mamun30-Apr-14 20:16 
GeneralRe: My vote of 5 Pin
John-ph3-May-14 22:06
John-ph3-May-14 22:06 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.