This version of the manual refers to an earlier version of the software.

Access Control

Authentication

In standalone mode, Cantaloupe has built-in support for HTTP Basic authentication. To enable it, set the following keys in the configuration file:

auth.basic.enabled = true
auth.basic.username = myusername
auth.basic.secret = mypassword

When enabled, the entire website and all endpoints will be restricted.


Authorization

A custom delegate script method can be used to implement authorization logic ranging from simple to complex. The image server will execute this method upon every image request and, depending on its return value, either authorize the request (by returning HTTP 200 OK), or not (by returning HTTP 403 Forbidden).

The delegate method in question is called authorized?. A skeleton with documented parameters and return values is present in the delegates.rb.sample file. By default, it just returns true, authorizing all requests.

Notes

  • The authorization method will be called on every image request and should therefore be written to be efficient.
  • The authorization method will be called upon requests to all image endpoints, but not information endpoints.
  • Implementations should not assume that the underlying source image actually exists, but they should not try to check for it regardless—the image server will handle that. (But, the check may occur after the delegate method is invoked.)

Examples

Inspect the parameters

This method will print the parameters to the console.

module Cantaloupe
  def self.authorized?(identifier, full_size, operations, resulting_size,
                       output_format, request_uri, request_headers, client_ip,
                       cookies)
    puts "identifier: #{identifier}"
    puts "full_size: #{full_size}"
    puts "operations: #{operations}"
    puts "resulting_size: #{resulting_size}"
    puts "output_format: #{output_format}"
    puts "request_uri: #{request_uri}"
    puts "request_headers: #{request_headers}"
    puts "client_ip: #{client_ip}"
    puts "cookies: #{cookies}"
    true
  end
end

Allow only requests for half-scale images or smaller

module Cantaloupe
  def self.authorized?(identifier, full_size, operations, resulting_size,
                       output_format, request_uri, request_headers, client_ip,
                       cookies)
    scale = operations.select{ |op| op['type'] == 'scale' }.first
    if scale
      max_scale = 0.5
      return scale['width'] <= full_size['width'] * max_scale and
          scale['height'] <= full_size['height'] * max_scale
    end
    false
  end
end

Allow only requests for identifiers matching a certain pattern

module Cantaloupe
  def self.authorized?(identifier, full_size, operations, resulting_size,
                       output_format, request_uri, request_headers, client_ip,
                       cookies)
    # Allow only identifiers that don't include "_restricted"
    return !identifier.include?('_restricted')
    # Allow only identifiers that start with "_public"
    return identifier.start_with?('public_')
    # Allow only identifiers matching a regex
    return identifier.match(/^image[5-9][0-9]/)
  end
end

Allow only requests for images set as "public" in a MySQL database

(You will need to install the MySQL JDBC driver first.)

Note: The parameters passed to authorized? are not guaranteed to be safe. identifier, for example, will be exactly as supplied in the URL. Always prefer prepared statements over string concatenation in order to reduce susceptibility to SQL injection attacks.
module Cantaloupe
  def self.authorized?(identifier, full_size, operations, resulting_size,
                       output_format, request_uri, request_headers, client_ip,
                       cookies)
    require "rubygems"
    require "jdbc/mysql"
    require "java"

    authorized = false

    Java::com.mysql.jdbc.Driver
    url = "jdbc:mysql://HOST/DATABASE"
    conn = java.sql.DriverManager.get_connection(url, "USERNAME", "PASSWORD")
    stmt = conn.create_statement

    begin
      query = %q{SELECT is_public
          FROM image
          WHERE identifier = ?
          LIMIT 1}
      stmt = conn.prepare_statement(query)
      stmt.setString(1, identifier);
      result_set = stmt.execute_query
      while result_set.next do
        authorized = result_set.getBoolean(1)
      end
    ensure
      stmt.close
      conn.close
    end
    authorized
  end
end

Allow only JPEG output

module Cantaloupe
  def self.authorized?(identifier, full_size, operations, resulting_size,
                       output_format, request_uri, request_headers, client_ip,
                       cookies)
    output_format['media_type'] == 'image/jpeg'
  end
end

Allow only certain user agents

This is not foolproof—if a client knows what User-Agent you are checking for, they can spoof it.
module Cantaloupe
  def self.authorized?(identifier, full_size, operations, resulting_size,
                       output_format, request_uri, request_headers, client_ip,
                       cookies)
    agent = request_headers.select{ |h, v| h.downcase == 'user-agent' }.first
    agent == 'MyAllowedUserAgent/1.0'
  end
end

Allow only requests from clients that have an authorization cookie

If you have an authorization service that sets a cookie, you can check for it. Cookies can't be shared across domains, but this could still work if you can set the cookie on a parent domain.

module Cantaloupe
  def self.authorized?(identifier, full_size, operations, resulting_size,
                       output_format, request_uri, request_headers, client_ip,
                       cookies)
    cookies.select{ |c| c['authcookie'] }.any?
  end
end

Restrict a region of an image

In this example, requests for images containing any part of the bottom right quadrant of the source image will be denied.

(Also see redaction.)

module Cantaloupe
  def self.authorized?(identifier, full_size, operations, resulting_size,
                       output_format, request_uri, request_headers, client_ip,
                       cookies)
    crop = operations.select{ |op| op['type'] == 'crop' }.first
    if crop
      max_x = full_size['width'] / 2
      max_y = full_size['height'] / 2
      return !(crop['x'] + crop['width'] > max_x and
          crop['y'] + crop['height'] > max_y)
    end
    false
  end
end