CKFinder 3 – ASP.NET Connector Documentation
HOWTO

If you did not find the answer you are looking for, send us your question: http://cksource.com/contact

Implementing Authenticator

By default CKFinder denies access to its interface to everyone.

To add your authenticator, implement the IAuthenticator interface and set it in the ConnectorBuilder.SetAuthenticator method.

The authenticator should determine whether the user doing the request can access CKFinder and it should assign this user roles.

The simplest implementation may look like this:

using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
public class MyAuthenticator : IAuthenticator
{
/*
* Although this method is asynchronous, it will be called for every request
* and it is not recommended to make time-consuming calls within it.
*/
public Task<IUser> AuthenticateAsync(ICommandRequest commandRequest, CancellationToken cancellationToken)
{
/*
* It should be safe to assume the IPrincipal is a ClaimsPrincipal.
*/
var claimsPrincipal = commandRequest.Principal as ClaimsPrincipal;
/*
* Extract role names from claimsPrincipal.
*/
var roles = claimsPrincipal?.Claims?.Where(x => x.Type == ClaimTypes.Role).Select(x => x.Value).ToArray();
/*
* It is strongly suggested to change this in a way to allow only certain users access to CKFinder.
* For example you may check commandRequest.RemoteIPAddress to limit access only to your local network.
*/
var isAuthenticated = true;
/*
* Create and return the user.
*/
var user = new User(isAuthenticated, roles);
return Task.FromResult((IUser)user);
}
}

Different Folder per Instance

If you use several CKFinder instances, you can use different id attributes and pass them to the server connector requests.

CKFinder.start( { id: 'instanceNo1', pass: 'id' } );
CKFinder.start( { id: 'instanceNo2', pass: 'id' } );

On connector side you can get the name of the current instance inside the action passed to SetRequestConfiguration with request.QueryParameters["id"].FirstOrDefault(), and use it for dynamic configuration modification. This way you can make each instance use its own root folder for the local file system backend.

connectorBuilder
.SetRequestConfiguration(
(request, config) =>
{
var instanceId = request.QueryParameters["id"].FirstOrDefault() ?? string.Empty;
var root = GetRootByInstanceId(instanceId);
var baseUrl = GetBaseUrlByInstanceId(instanceId);
config.AddProxyBackend("default", new LocalStorage(root));
});

For security reasons you should avoid using the instance name directly in the directory path and use a kind of a whitelist. The GetRootByInstanceId() method used in the configuration example above may look as follows:

private static string GetRootByInstanceId(string instanceId)
{
var pathMap = new Dictionary<string, string>
{
{ "instanceNo1", @"C:\Files\No1" },
{ "instanceNo2", @"C:\Files\No2" }
};
string root;
if (pathMap.TryGetValue(instanceId, out root))
{
return root;
}
throw new CustomErrorException("Invalid instance Id");
}

Private Folders per User

To create separate directories for users, you need to create a simple mechanism to map the current user to an appropriate directory path.

When building the directory path, you should remember about the following that may lead to path traversal attacks:

  • Do not reveal any sensitive information.
  • Do not use any insecure data.

In this example a sha1 hash of the current user name is used.

Note: When creating private directories for users you should also remember about internal settings like thumbnail and key-value store provider, which should be separated, too.

connectorBuilder.SetRequestConfiguration(
(request, config) =>
{
var userName = request.Principal?.Identity?.Name;
if (userName != null)
{
var sha = new SHA1CryptoServiceProvider();
var hash = sha.ComputeHash(Encoding.UTF8.GetBytes(userName));
var folderName = BitConverter.ToString(hash).Replace("-", string.Empty);
config.AddResourceType("private", builder => builder.SetBackend("default", folderName));
config.SetThumbnailBackend("default", $"App_Data/{folderName}");
config.SetKeyValueStoreProvider(new EntityFrameworkKeyValueStoreProvider(
"CacheConnectionString", string.Empty, folderName));
}
})

Disk Quota

In this example it is assumed that you have implemented your own logic for checking user storage quota with the IsQuotaAvailable() method. You can attach this logic to before command events in CKFinder for commands that you want to check (in case of checking quota: commands like FileUpload, CopyFiles, ImageResize, CreateFolder).

See the DiskQuota plugin sample for the source code implementing this functionality.

Logging User Actions

In this example the goal is to create a plugin for logging user actions. This can be achieved using the events system. For the purpose of this example let us assume that all user actions corresponding to intermediate events are to be logged. In order to do that, simple event listeners need to be created and attached to the events that should be logged.

See the UserActionsLogger plugin sample for the complete source code implementing this functionality.

If the plugin was registered properly, you should see output similar to below in your log file.

2016-02-24 11:24:40.0063 | INFO | dummyUser1 - Folder create: Files://folder/
2016-02-24 11:24:51.5327 | INFO | dummyUser1 - File upload: Files://folder/image.jpg
2016-02-24 11:25:10.1064 | INFO | dummyUser1 - File rename: Files://folder/image.jpg -> Files://folder/image2.jpg
2016-02-24 11:25:25.7100 | INFO | dummyUser1 - File move: Files://document.txt -> Files://folder/document.txt
2016-02-24 11:25:43.9000 | INFO | dummyUser1 - File copy: Files://folder/image.jpg -> Files://image.jpg
2016-02-24 11:25:49.6668 | INFO | dummyUser1 - File delete: Files://folder/image.jpg

Please refer to the Events section for more detailed information about types of event object parameters passed for particular events.

Custom Commands

This example presents a simple command plugin that returns basic information about a file.

See the GetFileInfo plugin sample for the complete source code implementing this functionality.

If this plugin is enabled, you can call an additional GetFileInfo command that returns some very basic information about a file, like the size and the last modification timestamp. This behavior can be simply altered to return any other information about the file (for example EXIF data for images or ID3 tags for mp3 files).

GetFileInfo

Description Returns basic information about a file.
Method GET
Sample request Get basic information about the foo.png file located in the sub1 directory of the Files resource type.
/ckfinder/connector?command=GetFileInfo&type=Files&currentFolder=/sub1/&fileName=foo.png
Sample response
{
"name":"foo.png",
"createDate":"20160128084240",
"updateDate":"20160128084240",
"size":27511,
"mimeType":"image/png"
}

For more detailed information about commands, please refer to the Commands section of the CKFinder ASP.NET connector documentation.

Pointing Resource Type to an Existing Folder

The resource type folder can be defined with the SetBackend method during the execution of the SetRequestConfiguration action defined in ConnectorBuilder or with the folder configuration option (see Resource Types). The defined directory is relative to the root of the backend.

Consider the following folder structure:

rootDir
└── dir1
└── dir2
└── dir3

where the rootDir is the root directory defined for the backend named default.

The resource type can be attached to the root folder simply by passing / as the second parameter to the SetBackend method:

config
.AddResourceType("Files", resourceBuilder =>
resourceBuilder.SetBackend("default", "/"));

Or by providing the / value to the folder configuration option:

<resourceType name="Files" folder="/" backend="default" />

With above configuration you will see the following folder tree in CKFinder:

Files
└── dir1
└── dir2
└── dir3

You can point the resource type to any subfolder, as presented below:

config
.AddResourceType("Files", resourceBuilder =>
resourceBuilder.SetBackend("default", "/dir1"));

With the folder option:

<resourceType name="Files" folder="/dir1" backend="default" />
Files
└── dir2
└── dir3

Or to point to a deeper subfolder:

config
.AddResourceType("Files", resourceBuilder =>
resourceBuilder.SetBackend("default", "/dir1/dir2"));
<resourceType name="Files" folder="/dir1/dir2" backend="default" />
Files
└── dir3

Using Zip Package Without Conversion to Application or Within WebMatrix

The easiest way to use the .zip package without conversion to application or in a WebMatrix is to extract the .zip archive contents into an empty site with the omission of the root ckfinder folder.

Next open the Web.config file and change:

<add key="ckfinderRoute" value="/connector" />

to:

<add key="ckfinderRoute" value="/ckfinder/connector" />

in the <appSettings /> section.

CKFinder and Classic ASP

Classic ASP is not supported by CKFinder 3.x, however, it is possible to obtain user authentication data for CKFinder through a custom Authenticator class and an additional ASP script in your classic ASP application.

The additional ASP script should return JSON data with user's authentication data. It should be placed in a publicly visible place. It may look like this:

<%@Language=VBScript CodePage=65001%>
<% Option Explicit %>
<%
' For security reasons allow only local requests.
' If remote requests are needed, you should use some kind of a secret key or a certificate.
If Request.ServerVariables("LOCAL_ADDR") <> Request.ServerVariables("REMOTE_ADDR") Then
Response.Status = "403 Forbidden"
Response.End
End If
Dim isAuthenticated
Dim roles
' Assign True to isAuthenticated if the user is allowed to access CKFinder.
isAuthenticated = False
' Assign user roles to the roles array.
' For example:
' roles = Array("Administrator", "Manager")
Dim quotedRoles
ReDim quotedRoles(uBound(roles))
Dim role
Dim index
index = 0
For Each role In roles
quotedRoles(index) = """" & role & """"
index = index + 1
Next
Response.ContentType = "application/json"
Response.Charset = "utf-8"
Response.Write "{ ""isAuthenticated"": "
Response.Write """" & isAuthenticated & """"
Response.Write ", ""roles"": [ "
Response.Write Join(quotedRoles, ", ")
Response.Write " ] }"
%>

The custom Authenticator class may look like this:

using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
public class ClassicAspAuthenticator : IAuthenticator
{
private readonly string _classicAspConnectorUrl;
public ClassicAspAuthenticator(string classicAspConnectorUrl)
{
_classicAspConnectorUrl = classicAspConnectorUrl;
}
public async Task<IUser> AuthenticateAsync(ICommandRequest commandRequest, CancellationToken cancellationToken)
{
var httpClient = new HttpClient();
var response = await httpClient.GetAsync(_classicAspConnectorUrl, cancellationToken);
var json = await response.Content.ReadAsStringAsync();
return json.FromJson<User>();
}
}

The last step is to pass the ClassicAspAuthenticator instance in the ConnectorBuilder.SetAuthenticator method:

var authenticator = new ClassicAspAuthenticator("http://url/to/the/additional/classic/asp/script.asp");
var connectorBuilder = new ConnectorBuilder();
connectorBuilder.SetAuthenticator(authenticator);

Mixing Multiple Owin Middlewares

When you want to mix CKFinder middleware with other middlewares you can do so through route mapping:

public void Configuration(IAppBuilder appBuilder)
{
var connectorBuilder = ConfigureConnector();
var connector = connectorBuilder.Build(new OwinConnectorFactory());
appBuilder.Map("/CKFinder/connector", builder => builder.UseConnector(connector));
appBuilder.Map("/anotherMiddleware", builder => builder.UseAnotherMiddleware());
}

For more information about Owin route mapping see the AppBuilder class reference on MSDN.

For more information about integration with existing application see Integrating in Existing Application.

Adding Support for Custom Storage

Support for a custom file system may be added with the implementation of the IFileSystem interface.

Most of the members of this interface are self-explanatory, however four methods require a few additional words:

Task<FolderListResult> GetFolderInfosAsync(string path, CancellationToken cancellationToken);
Task<FolderListResult> GetFolderInfosAsync(IFolderListContinuation folderListContinuation, CancellationToken cancellationToken);
Task<FileListResult> GetFileInfosAsync(string path, CancellationToken cancellationToken);
Task<FileListResult> GetFileInfosAsync(IFileListContinuation fileListContinuation, CancellationToken cancellationToken);

These four members are responsible for listing folders and files. It is assumed that calls with the path as a parameter are always first requests and subsequent calls are called with continuation objects. These continuation objects are cursors and should be handled internally by the file system's implementation.

The sample adapter supports storage in the database.

For the purpose of this tutorial let us assume that the files will be stored in one database table, represented by the SQL schema shown below:

SQL Server

CREATE TABLE [dbo].[DatabaseNodes](
[Id] [int] IDENTITY(1,1) NOT NULL,
[Path] [nvarchar](max) NULL,
[Type] [int] NOT NULL,
[Contents] [varbinary](max) NULL,
[Size] [int] NOT NULL,
[MimeType] [nvarchar](max) NULL,
[Timestamp] [datetime] NOT NULL,
CONSTRAINT [PK_dbo.DatabaseNodes] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

Implementing a Custom Storage

The first step in adding a custom storage in the CKFinder 3 ASP.NET connector is creating an implementation of IFileSystem. This interface defines all the methods that are required to communicate with the given file system — for example writing, reading or deleting a file.

Have a look at a custom implementation of IFileSystem required to save files in a database table with the assumed schema. The DatabaseStorage class uses the EntityFramework to communicate with the database. The instantiation of the DatabaseStorage class is presented below.

var databaseStorage = new DatabaseStorage("Server=myServerAddress;Database=myDataBase;User Id=myUsername;Password=myPassword;");

Registering a Custom Adapter for the <tt>web.config</tt> Configuration

To register a custom adapter for use in the static web.config configuration you have to define how this adapter is created. This is done in the static FileSystemFactory class. For the DatabaseStorage class that requires only the connection string parameter, this can be done as in the example below:

FileSystemFactory.RegisterFileSystem("local", options => new DatabaseStorage(options["connectionString"]));

See the DatabaseStorage sample for the complete source code implementing this functionality.

Setting License Details per Request

To set license details per request it is required to dynamically alter the connector configuration in code (see Configuration by Code).

License details can be altered per request in a callback passed to the connectorBuilder.SetRequestConfiguration() method, as shown in the example below:

var connector = connectorBuilder
.LoadConfig()
.SetRequestConfiguration(
(request, config) =>
{
config.LoadConfig();
// Sets licenseName and licenseKey to use in the current request.
connectorBuilder.licenseProvider.SetLicense(licenseName, licenseKey);
})
.Build(connectorFactory);

Defining a Custom S3 Client in Amazon S3 Adapter

To define a custom S3 Client for Amazon S3 adapter, extend the default IFileSystem class and overwrite the createClient() factory method, like presented below.

public class CustomS3Storage : AmazonStorage
{
public CustomS3Storage() : base("bucket-name")
{
}
protected override AmazonS3Client createClient()
{
BasicAWSCredentials credentials = new BasicAWSCredentials("key", "secret");
AmazonS3Config config = new AmazonS3Config();
config.RegionEndpoint = RegionEndpoint.GetBySystemName("region-name");
config.SignatureVersion = "4";
return new AmazonS3Client(credentials, config);
}
}

Then you can register the new storage type in connector:

connectorBuilder
.LoadConfig()
.SetRequestConfiguration(
(request, config) =>
{
config.LoadConfig();
config.AddBackend("s3", new CustomS3Storage());
config.AddResourceType("S3 Resorce Type", resourceBuilder => {
resourceBuilder.SetBackend("s3", "");
resourceBuilder.SetLazyLoaded(true);
});
}

Securing a Publicly Accessible Folder

When integrating CKFinder, you will often want to give users access to uploaded files, so they can insert images or links to files into the edited content. This can be done in two ways:

  • You can configure your CKFinder to serve all files through the connector using the Proxy command.
  • You can make the folder publicly accessible, so all the files are served through the web server.

If you rely on your web server to serve the files uploaded with CKFinder, you should take additional steps to make sure the files are served in a secure way.

Let us assume that you have configured your CKFinder to allow uploading of .avi files.

Even if the .avi file is then served with a valid Content-Type: video/x-msvideo header, some browsers may ignore this information and perform additional checks on the raw file contents. If any HTML-like data is detected in the file content, the browser may decide to ignore information about the content type and handle the served content as if it was a regular web page. This behavior is called "content sniffing" (also known as "media type sniffing" or "MIME sniffing"), and in some circumstances it may lead to security issues (for example, it may open door for XSS attacks).

To avoid content sniffing, you should make sure that your server adds the X-Content-Type-Options: nosniff header to all HTTP responses when serving files from the publicly available folder. The X-Content-Type-Options response HTTP header is a marker used by the server to indicate that the MIME type set by the Content-Type header should not be changed and should be followed. As a result, the browser does not perform any content sniffing on the received content.

Microsoft IIS

For Microsoft IIS servers, you can enable the X-Content-Type-Options header in your web.config file:

<system.webServer>
<httpProtocol>
<customHeaders>
<remove name="X-Content-Type-Options"/>
<add name="X-Content-Type-Options" value="nosniff"/>
</customHeaders>
</httpProtocol>
</system.webServer>

Apache

If you use the Apache web server, you can add custom HTTP response headers using mod_headers. Make sure the mod_headers module is enabled, and create (or modify) the following .htaccess file in the root of the publicly accessible folder (for example userfiles/.htaccess):

Header set X-Content-Type-Options "nosniff"

Nginx

If you use Nginx, custom HTTP response headers can be defined per location:

location /userfiles {
add_header X-Content-Type-Options nosniff;
}