aboutsummaryrefslogtreecommitdiffhomepage
path: root/application/api/ApiMiddleware.php
blob: 7f1e7fca2785d0d485d9855ccdcd4899ba09feae (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
<?php
namespace Shaarli\Api;

use Shaarli\Api\Exceptions\ApiAuthorizationException;
use Shaarli\Api\Exceptions\ApiException;
use Shaarli\Config\ConfigManager;
use Slim\Container;
use Slim\Http\Request;
use Slim\Http\Response;

/**
 * Class ApiMiddleware
 *
 * This will be called before accessing any API Controller.
 * Its role is to make sure that the API is enabled, configured, and to validate the JWT token.
 *
 * If the request is validated, the controller is called, otherwise a JSON error response is returned.
 *
 * @package Api
 */
class ApiMiddleware
{
    /**
     * @var int JWT token validity in seconds (9 min).
     */
    public static $TOKEN_DURATION = 540;

    /**
     * @var Container: contains conf, plugins, etc.
     */
    protected $container;

    /**
     * @var ConfigManager instance.
     */
    protected $conf;

    /**
     * ApiMiddleware constructor.
     *
     * @param Container $container instance.
     */
    public function __construct($container)
    {
        $this->container = $container;
        $this->conf = $this->container->get('conf');
        $this->setLinkDb($this->conf);
    }

    /**
     * Middleware execution:
     *   - check the API request
     *   - execute the controller
     *   - return the response
     *
     * @param  Request  $request  Slim request
     * @param  Response $response Slim response
     * @param  callable $next     Next action
     *
     * @return Response response.
     */
    public function __invoke($request, $response, $next)
    {
        try {
            $this->checkRequest($request);
            $response = $next($request, $response);
        } catch (ApiException $e) {
            $e->setResponse($response);
            $e->setDebug($this->conf->get('dev.debug', false));
            $response = $e->getApiResponse();
        }

        return $response;
    }

    /**
     * Check the request validity (HTTP method, request value, etc.),
     * that the API is enabled, and the JWT token validity.
     *
     * @param  Request $request  Slim request
     *
     * @throws ApiAuthorizationException The API is disabled or the token is invalid.
     */
    protected function checkRequest($request)
    {
        if (! $this->conf->get('api.enabled', true)) {
            throw new ApiAuthorizationException('API is disabled');
        }
        $this->checkToken($request);
    }

    /**
     * Check that the JWT token is set and valid.
     * The API secret setting must be set.
     *
     * @param  Request $request  Slim request
     *
     * @throws ApiAuthorizationException The token couldn't be validated.
     */
    protected function checkToken($request)
    {
        if (! $request->hasHeader('Authorization') && !isset($this->container->environment['REDIRECT_HTTP_AUTHORIZATION'])) {
            throw new ApiAuthorizationException('JWT token not provided');
        }

        if (empty($this->conf->get('api.secret'))) {
            throw new ApiAuthorizationException('Token secret must be set in Shaarli\'s administration');
        }

        if (isset($this->container->environment['REDIRECT_HTTP_AUTHORIZATION'])) {
            $authorization = $this->container->environment['REDIRECT_HTTP_AUTHORIZATION'];
        } else {
            $authorization = $request->getHeaderLine('Authorization');
        }

        if (! preg_match('/^Bearer (.*)/i', $authorization, $matches)) {
            throw new ApiAuthorizationException('Invalid JWT header');
        }

        ApiUtils::validateJwtToken($matches[1], $this->conf->get('api.secret'));
    }

    /**
     * Instantiate a new LinkDB including private links,
     * and load in the Slim container.
     *
     * FIXME! LinkDB could use a refactoring to avoid this trick.
     *
     * @param ConfigManager $conf instance.
     */
    protected function setLinkDb($conf)
    {
        $linkDb = new \Shaarli\Bookmark\LinkDB(
            $conf->get('resource.datastore'),
            true,
            $conf->get('privacy.hide_public_links')
        );
        $this->container['db'] = $linkDb;
    }
}