blob: 38a3300c35840e92b23576d4fa6e55c1fb67ea6b [file] [log] [blame]
Akron0e1ed242018-10-11 13:22:00 +02001#!/usr/bin/env perl
2use Mojolicious::Lite;
3use Mojo::ByteStream 'b';
4use Mojo::Date;
5use Mojo::JSON qw/true false encode_json decode_json/;
6use strict;
7use warnings;
8use Mojo::JWT;
Akron6d49c1f2018-10-11 14:22:21 +02009use Mojo::File qw/path/;
10use Mojo::Util qw/slugify/;
Akron0e1ed242018-10-11 13:22:00 +020011
12# This is an API fake server with fixtures
13
14my $secret = 's3cr3t';
Akron73f36082018-10-25 15:34:59 +020015my $fixture_path = path(Mojo::File->new(__FILE__)->dirname)->child('..', 'fixtures');
Akron0e1ed242018-10-11 13:22:00 +020016
Akroncdfd9d52019-07-23 11:35:00 +020017our %tokens = (
Akron59992122019-10-29 11:28:45 +010018 'access_token' => "4dcf8784ccfd26fac9bdb82778fe60e2",
19 'refresh_token' => "hlWci75xb8atDiq3924NUSvOdtAh7Nlf9z",
20 'access_token_2' => "abcde",
21 'refresh_token_2' => "fghijk",
22 'new_client_id' => 'fCBbQkA2NDA3MzM1Yw==',
23 'new_client_secret' => 'KUMaFxs6R1WGud4HM22w3HbmYKHMnNHIiLJ2ihaWtB4N5JxGzZgyqs5GTLutrORj',
Akroncdfd9d52019-07-23 11:35:00 +020024);
25
26helper get_token => sub {
27 my ($c, $token) = @_;
28 return $tokens{$token}
29};
30
Akron33f5c672019-06-24 19:40:47 +020031# Legacy:
Akron0e1ed242018-10-11 13:22:00 +020032helper jwt_encode => sub {
33 shift;
34 return Mojo::JWT->new(
35 secret => $secret,
36 token_type => 'api_token',
37 expires => time + (3 * 34 * 60 * 60),
38 claims => { @_ }
39 );
40};
41
Akron33f5c672019-06-24 19:40:47 +020042# Legacy;
Akron0e1ed242018-10-11 13:22:00 +020043helper jwt_decode => sub {
44 my ($c, $auth) = @_;
45 $auth =~ s/\s*api_token\s+//;
46 return Mojo::JWT->new(secret => $secret)->decode($auth);
47};
48
Akroncdfd9d52019-07-23 11:35:00 +020049# Expiration helper
50helper expired => sub {
51 my ($c, $auth, $set) = @_;
52
53
54 $auth =~ s/^[^ ]+? //;
55 if ($set) {
56 $c->app->log->debug("Set $auth for expiration");
57 $c->app->defaults('auth_' . $auth => 1);
58 return 1;
59 };
60
61 $c->app->log->debug("Check $auth for expiration: " . (
62 $c->app->defaults('auth_' . $auth) // '0'
63 ));
64
65 return $c->app->defaults('auth_' . $auth);
66};
Akron0e1ed242018-10-11 13:22:00 +020067
Akron6d49c1f2018-10-11 14:22:21 +020068# Load fixture responses
69helper 'load_response' => sub {
70 my $c = shift;
71 my $q_name = shift;
72 my $file = $fixture_path->child("response_$q_name.json");
Akron8ea84292018-10-24 13:41:52 +020073 $c->app->log->debug("Load response from $file");
74
Akron6d49c1f2018-10-11 14:22:21 +020075 unless (-f $file) {
76 return {
77 status => 500,
78 json => {
79 errors => [[0, 'Unable to load query response from ' . $file]]
80 }
81 }
82 };
Akron8ea84292018-10-24 13:41:52 +020083
Akron6d49c1f2018-10-11 14:22:21 +020084 my $response = $file->slurp;
Akrona3c353c2019-02-14 23:50:00 +010085 my $decode = decode_json($response);
86 unless ($decode) {
87 return {
88 status => 500,
89 json => {
90 errors => [[0, 'Unable to parse JSON']]
91 }
92 }
93 };
94
95 return $decode;
Akron6d49c1f2018-10-11 14:22:21 +020096};
97
Akron1a9d5be2020-03-19 17:28:33 +010098app->defaults('oauth.client_list' => []);
99
Akron6d49c1f2018-10-11 14:22:21 +0200100
Akron0e1ed242018-10-11 13:22:00 +0200101# Base page
Akron63d963b2019-07-05 15:35:51 +0200102get '/v1.0/' => sub {
Akron6d49c1f2018-10-11 14:22:21 +0200103 shift->render(text => 'Fake server available');
Akron0e1ed242018-10-11 13:22:00 +0200104};
105
Akron32396632018-10-11 17:08:37 +0200106
Akrond00b4272020-02-05 17:00:33 +0100107get '/v1.0/redirect-target-a' => sub {
108 shift->render(text => 'Redirect Target!');
109} => 'redirect-target';
110
111
112# Base page
113get '/v1.0/redirect' => sub {
114 my $c = shift;
115 $c->res->code(308);
116 $c->res->headers->location($c->url_for('redirect-target')->to_abs);
117 return $c->render(text => '');
118};
119
120
Akron0e1ed242018-10-11 13:22:00 +0200121# Search fixtures
Akron63d963b2019-07-05 15:35:51 +0200122get '/v1.0/search' => sub {
Akron0e1ed242018-10-11 13:22:00 +0200123 my $c = shift;
124 my $v = $c->validation;
125 $v->optional('q');
126 $v->optional('page');
127 $v->optional('ql');
Akroncd42a142019-07-12 18:55:37 +0200128 $v->optional('cq');
Akron0e1ed242018-10-11 13:22:00 +0200129 $v->optional('count');
130 $v->optional('context');
Akron8ea84292018-10-24 13:41:52 +0200131 $v->optional('offset');
132 $v->optional('cutoff')->in(qw/true false/);
Akron0e1ed242018-10-11 13:22:00 +0200133
Akron32396632018-10-11 17:08:37 +0200134 $c->app->log->debug('Receive request');
135
Akron0e1ed242018-10-11 13:22:00 +0200136 # Response q=x&ql=cosmas3
137 if ($v->param('ql') && $v->param('ql') eq 'cosmas3') {
138 return $c->render(
139 status => 400,
140 json => {
141 "\@context" => "http://korap.ids-mannheim.de/ns/koral/0.3/context.jsonld",
142 "errors" => [[307,"cosmas3 is not a supported query language!"]]
143 });
144 };
145
Akron6d49c1f2018-10-11 14:22:21 +0200146 if (!$v->param('q')) {
Akron8ea84292018-10-24 13:41:52 +0200147 return $c->render(%{$c->load_response('query_no_query')});
Akron0e1ed242018-10-11 13:22:00 +0200148 };
149
Akron8ea84292018-10-24 13:41:52 +0200150 my @slug_base = ($v->param('q'));
151 push @slug_base, 'o' . $v->param('offset') if defined $v->param('offset');
152 push @slug_base, 'c' . $v->param('count') if defined $v->param('count');
153 push @slug_base, 'co' . $v->param('cutoff') if defined $v->param('cutoff');
Akroncd42a142019-07-12 18:55:37 +0200154 push @slug_base, 'cq' if defined $v->param('cq');
Akron8ea84292018-10-24 13:41:52 +0200155
Akron6d49c1f2018-10-11 14:22:21 +0200156 # Get response based on query parameter
Akron8ea84292018-10-24 13:41:52 +0200157 my $response = $c->load_response('query_' . slugify(join('_', @slug_base)));
Akron0e1ed242018-10-11 13:22:00 +0200158
159 # Check authentification
160 if (my $auth = $c->req->headers->header('Authorization')) {
Akron33f5c672019-06-24 19:40:47 +0200161
Akroncdfd9d52019-07-23 11:35:00 +0200162 $c->app->log->debug("There is an authorization header $auth");
Akron33f5c672019-06-24 19:40:47 +0200163 my $jwt;
164 if ($auth =~ /^Bearer/) {
165 # Username unknown in OAuth2
166 $response->{json}->{meta}->{authorized} = 'yes';
167 }
Akroncdfd9d52019-07-23 11:35:00 +0200168 elsif ($auth =~ /^api_token/ && ($jwt = $c->jwt_decode($auth))) {
Akron6d49c1f2018-10-11 14:22:21 +0200169 $response->{json}->{meta}->{authorized} = $jwt->{username} if $jwt->{username};
Akron0e1ed242018-10-11 13:22:00 +0200170 };
Akroncdfd9d52019-07-23 11:35:00 +0200171
172 # Code is expired
173 if ($c->expired($auth)) {
174
175 $c->app->log->debug("The access token has expired");
176
177 return $c->render(
178 status => 401,
179 json => {
180 errors => [[2003, 'Access token is expired']]
181 }
182 );
183 }
184
185 # Auth token is invalid
186 if ($auth =~ /^Bearer inv4lid/) {
187 $c->app->log->debug("The access token is invalid");
188
189 return $c->render(
190 status => 401,
191 json => {
192 errors => [[2011, 'Access token is invalid']]
193 }
194 );
195 }
Akron0e1ed242018-10-11 13:22:00 +0200196 };
197
Akron6d49c1f2018-10-11 14:22:21 +0200198 # Set page parameter
Akron0e1ed242018-10-11 13:22:00 +0200199 if ($v->param('page')) {
Akron6d49c1f2018-10-11 14:22:21 +0200200 $response->{json}->{meta}->{startIndex} = $v->param("startIndex");
Akron0e1ed242018-10-11 13:22:00 +0200201 };
202
Akron0e1ed242018-10-11 13:22:00 +0200203 # Simple search fixture
Akron32396632018-10-11 17:08:37 +0200204 $c->render(%$response);
205
206 $c->app->log->debug('Rendered result');
207
208 return 1;
Akron0e1ed242018-10-11 13:22:00 +0200209};
210
Akron80a84b22018-10-24 17:44:24 +0200211# Textinfo fixtures
Akron63d963b2019-07-05 15:35:51 +0200212get '/v1.0/corpus/:corpusId/:docId/:textId' => sub {
Akron80a84b22018-10-24 17:44:24 +0200213 my $c = shift;
214
215 my $file = join('_', (
216 'textinfo',
217 $c->stash('corpusId'),
218 $c->stash('docId'),
219 $c->stash('textId')
220 ));
221
222 my $slug = slugify($file);
223
224 # Get response based on query parameter
225 my $response = $c->load_response($slug);
226 return $c->render(%$response);
227};
228
Akron0e1ed242018-10-11 13:22:00 +0200229
Akronb80341d2018-10-15 19:46:23 +0200230# Matchinfo fixtures
Akron63d963b2019-07-05 15:35:51 +0200231get '/v1.0/corpus/:corpusId/:docId/:textId/:matchId/matchInfo' => sub {
Akronb80341d2018-10-15 19:46:23 +0200232 my $c = shift;
233
234 my $file = join('_', (
235 'matchinfo',
236 $c->stash('corpusId'),
237 $c->stash('docId'),
238 $c->stash('textId'),
239 $c->stash('matchId')
240 ));
241
Akronb8d0b402018-10-18 23:51:52 +0200242 my $slug = slugify($file);
243
Akronb80341d2018-10-15 19:46:23 +0200244 # Get response based on query parameter
Akronb8d0b402018-10-18 23:51:52 +0200245 my $response = $c->load_response($slug);
Akronb80341d2018-10-15 19:46:23 +0200246 return $c->render(%$response);
247};
248
Akron0e1ed242018-10-11 13:22:00 +0200249
Akronbe61f4c2018-10-20 00:52:58 +0200250# Statistics endpoint
Akron63d963b2019-07-05 15:35:51 +0200251get '/v1.0/statistics' => sub {
Akronbe61f4c2018-10-20 00:52:58 +0200252 my $c = shift;
253 my $v = $c->validation;
Akron5fa61e92019-07-15 11:56:11 +0200254 $v->optional('cq');
Akronbe61f4c2018-10-20 00:52:58 +0200255
256 my @list = 'corpusinfo';
Akron5fa61e92019-07-15 11:56:11 +0200257 if ($v->param('cq')) {
258 push @list, $v->param('cq');
Akronbe61f4c2018-10-20 00:52:58 +0200259 };
260 my $slug = slugify(join('_', @list));
261
262 # Get response based on query parameter
263 my $response = $c->load_response($slug);
264 return $c->render(%$response);
265};
266
Akron0e1ed242018-10-11 13:22:00 +0200267############
268# Auth API #
269############
270
271# Request API token
Akron63d963b2019-07-05 15:35:51 +0200272get '/v1.0/auth/logout' => sub {
Akron0e1ed242018-10-11 13:22:00 +0200273 my $c = shift;
274
275 if (my $auth = $c->req->headers->header('Authorization')) {
Akroncdfd9d52019-07-23 11:35:00 +0200276
277 if ($auth =~ /^Bearer/) {
278 $c->app->log->debug('Server-Logout: ' . $auth);
279 return $c->render(json => { msg => [[0, 'Fine!']]});
280 }
281
282 elsif (my $jwt = $c->jwt_decode($auth)) {
Akron0e1ed242018-10-11 13:22:00 +0200283 my $user = $jwt->{username} if $jwt->{username};
284
285 $c->app->log->debug('Server-Logout: ' . $user);
286 return $c->render(json => { msg => [[0, 'Fine!']]});
287 };
288 };
289
290 return $c->render(status => 400, json => { error => [[0, 'No!']]});
291};
292
293
294# Request API token
Akron63d963b2019-07-05 15:35:51 +0200295get '/v1.0/auth/apiToken' => sub {
Akron0e1ed242018-10-11 13:22:00 +0200296 my $c = shift;
297
298 # Get auth header
299 my $auth = $c->req->headers->authorization;
300
301 # Authorization missing or not basic
302 if (!$auth || $auth !~ s/\s*Basic\s+//gi) {
303 return $c->render(
304 json => {
305 error => [[2, 'x']]
306 }
307 );
308 };
309
310 # Decode header
311 my ($username, $pwd) = @{b($auth)->b64_decode->split(':')->to_array};
312
313 # the password is 'pass'
314 if ($pwd) {
315
316 # the password is 'pass'
317 if ($pwd eq 'pass') {
318
319 # Render info with token
320 my $jwt = $c->jwt_encode(username => $username);
321
322 # Render in the Kustvakt fashion:
323 return $c->render(
324 format => 'html',
325 text => encode_json({
326 %{$jwt->claims},
327 expires => $jwt->expires,
328 token => $jwt->encode,
329 token_type => 'api_token'
330 })
331 );
Akron3d673062019-01-29 15:54:16 +0100332 }
333
334 elsif ($pwd eq 'ldaperr') {
335 return $c->render(
336 format => 'html',
337 status => 401,
338 json => {
339 "errors" => [[2022,"LDAP Authentication failed due to unknown user or password!"]]
340 }
341 );
Akron0e1ed242018-10-11 13:22:00 +0200342 };
343
344 return $c->render(
345 json => {
346 error => [[2004, undef]]
347 }
348 );
349 };
350
351 return $c->render(
352 json => {
353 error => [[2004, undef]]
354 }
355 );
356};
357
Akron33f5c672019-06-24 19:40:47 +0200358
359# Request API token
Akron63d963b2019-07-05 15:35:51 +0200360post '/v1.0/oauth2/token' => sub {
Akron33f5c672019-06-24 19:40:47 +0200361 my $c = shift;
362
Akron63d963b2019-07-05 15:35:51 +0200363 my $grant_type = $c->param('grant_type') // 'undefined';
364
365 if ($grant_type eq 'password') {
Akron33f5c672019-06-24 19:40:47 +0200366
Akron8bbbecf2019-07-01 18:57:30 +0200367 # Check for wrong client id
368 if ($c->param('client_id') ne '2') {
369 return $c->render(
370 json => {
371 "error_description" => "Unknown client with " . $_->{client_id},
372 "error" => "invalid_client"
373 },
374 status => 401
375 );
376 }
Akron33f5c672019-06-24 19:40:47 +0200377
Akron8bbbecf2019-07-01 18:57:30 +0200378 # Check for wrong client secret
379 elsif ($c->param('client_secret') ne 'k414m4r-s3cr3t') {
380 return $c->render(
381 json => {
382 "error_description" => "Invalid client credentials",
383 "error" => "invalid_client"
384 },
385 status => 401
386 );
387 }
Akron33f5c672019-06-24 19:40:47 +0200388
Akron8bbbecf2019-07-01 18:57:30 +0200389 # Check for wrong user name
390 elsif ($c->param('username') ne 'test') {
391 return $c->render(json => {
392 error => [[2004, undef]]
393 });
394 }
395
396 # Check for ldap error
397 elsif ($c->param('password') eq 'ldaperr') {
398 return $c->render(
399 format => 'html',
400 status => 401,
401 json => {
402 "errors" => [
403 [
404 2022,
405 "LDAP Authentication failed due to unknown user or password!"
406 ]
407 ]
408 }
409 );
410 }
411
412 # Check for wrong password
413 elsif ($c->param('password') ne 'pass') {
414 return $c->render(json => {
415 format => 'html',
416 status => 401,
Akron33f5c672019-06-24 19:40:47 +0200417 "errors" => [[2022,"LDAP Authentication failed due to unknown user or password!"]]
Akron8bbbecf2019-07-01 18:57:30 +0200418 });
419 }
420
421 # Return fine access
422 return $c->render(
423 json => {
Akroncdfd9d52019-07-23 11:35:00 +0200424 "access_token" => $c->get_token('access_token'),
425 "refresh_token" => $c->get_token('refresh_token'),
Akron8bbbecf2019-07-01 18:57:30 +0200426 "scope" => "all",
427 "token_type" => "Bearer",
428 "expires_in" => 86400
429 });
430 }
431
432 # Refresh token
Akron63d963b2019-07-05 15:35:51 +0200433 elsif ($grant_type eq 'refresh_token') {
Akroncdfd9d52019-07-23 11:35:00 +0200434
435 if ($c->param('refresh_token') eq 'inv4lid') {
436 return $c->render(
437 status => 400,
438 json => {
439 "error_description" => "Refresh token is expired",
440 "error" => "invalid_grant"
441 }
442 );
443 };
444
445 $c->app->log->debug("Refresh the token in the mock server!");
446
Akron8bbbecf2019-07-01 18:57:30 +0200447 return $c->render(
448 status => 200,
449 json => {
Akroncdfd9d52019-07-23 11:35:00 +0200450 "access_token" => $c->get_token("access_token_2"),
451 "refresh_token" => $c->get_token("refresh_token_2"),
Akron8bbbecf2019-07-01 18:57:30 +0200452 "token_type" => "Bearer",
453 "expires_in" => 86400
Akron33f5c672019-06-24 19:40:47 +0200454 }
455 );
456 }
457
Akron8bbbecf2019-07-01 18:57:30 +0200458 # Unknown token grant
459 else {
460 return $c->render(
Akron63d963b2019-07-05 15:35:51 +0200461 status => 400,
Akron8bbbecf2019-07-01 18:57:30 +0200462 json => {
463 "errors" => [
464 [
Akron63d963b2019-07-05 15:35:51 +0200465 0, "Grant Type unknown", $grant_type
Akron8bbbecf2019-07-01 18:57:30 +0200466 ]
467 ]
468 }
469 )
Akron33f5c672019-06-24 19:40:47 +0200470 }
Akron33f5c672019-06-24 19:40:47 +0200471};
472
Akron4cefe1f2019-09-04 10:11:28 +0200473# Revoke API token
474post '/v1.0/oauth2/revoke' => sub {
475 my $c = shift;
476
477 my $refresh_token = $c->param('token');
478
479 if ($c->param('client_secret') ne 'k414m4r-s3cr3t') {
480 return $c->render(
481 json => {
482 "error_description" => "Invalid client credentials",
483 "error" => "invalid_client"
484 },
485 status => 401
486 );
487 };
488
489 return $c->render(
490 text => ''
491 )
492};
Akron33f5c672019-06-24 19:40:47 +0200493
Akron59992122019-10-29 11:28:45 +0100494# Register a client
495post '/v1.0/oauth2/client/register' => sub {
496 my $c = shift;
497 my $json = $c->req->json;
498
499 my $name = $json->{name};
Akron1a9d5be2020-03-19 17:28:33 +0100500 my $desc = $json->{description};
Akron59992122019-10-29 11:28:45 +0100501 my $type = $json->{type};
502 my $url = $json->{url};
503 my $redirect_url = $json->{redirectURI};
504
Akron1a9d5be2020-03-19 17:28:33 +0100505 my $list = $c->app->defaults('oauth.client_list');
506
507 push @$list, {
508 "clientId" => $tokens{new_client_id},
509 "clientName" => $name,
510 "description" => $desc,
511 "url" => $url
512 };
513
Akron59992122019-10-29 11:28:45 +0100514 # Confidential server application
515 if ($type eq 'CONFIDENTIAL') {
516 return $c->render(json => {
517 client_id => $tokens{new_client_id},
518 client_secret => $tokens{new_client_secret}
519 });
520 };
521
522 # Desktop application
523 return $c->render(json => {
524 client_id => $tokens{new_client_id}
525 });
526};
527
Akron33f5c672019-06-24 19:40:47 +0200528
Akron0f1b93b2020-03-17 11:37:19 +0100529# Register a client
530post '/v1.0/oauth2/client/list' => sub {
531 my $c = shift;
532
533 # $c->param('client_secret');
Akron1a9d5be2020-03-19 17:28:33 +0100534
535 # Is empty [] when nothing registered
536
Akron0f1b93b2020-03-17 11:37:19 +0100537 return $c->render(
Akron1a9d5be2020-03-19 17:28:33 +0100538 json => $c->stash('oauth.client_list'),
539 status => 200
540 );
541};
542
543del '/v1.0/oauth2/client/deregister/:client_id' => sub {
544 my $c = shift;
545 my $client_id = $c->stash('client_id');
546
547 my $list = $c->app->defaults('oauth.client_list');
548
549 my $break = -1;
550 for (my $i = 0; $i < @$list; $i++) {
551 if ($list->[$i]->{clientId} eq $client_id) {
552 $break = $i;
553 last;
554 };
555 };
556
557 if ($break != -1) {
558 splice @$list, $break, 1;
559 }
560
561 else {
562 return $c->render(
563 json => {
564 error_description => "Unknown client with $client_id.",
565 error => "invalid_client"
Akron0f1b93b2020-03-17 11:37:19 +0100566 },
Akron1a9d5be2020-03-19 17:28:33 +0100567 status => 401
568 );
569 };
570
571 return $c->render(
572 json => $c->stash('oauth.client_list'),
Akron0f1b93b2020-03-17 11:37:19 +0100573 status => 200
574 );
575};
576
577
578
Akron0e1ed242018-10-11 13:22:00 +0200579app->start;
580
581
582__END__
583
584
585 # Temporary:
586 my $collection_query = {
587 '@type' => "koral:docGroup",
588 "operation" => "operation:or",
589 "operands" => [
590 {
591 '@type' => "koral:docGroup",
592 "operation" => "operation:and",
593 "operands" => [
594 {
595 '@type' => "koral:doc",
596 "key" => "title",
597 "match" => "match:eq",
598 "value" => "Der Birnbaum",
599 "type" => "type:string"
600 },
601 {
602 '@type' => "koral:doc",
603 "key" => "pubPlace",
604 "match" => "match:eq",
605 "value" => "Mannheim",
606 "type" => "type:string"
607 },
608 {
609 '@type' => "koral:docGroup",
610 "operation" => "operation:or",
611 "operands" => [
612 {
613 '@type' => "koral:doc",
614 "key" => "subTitle",
615 "match" => "match:eq",
616 "value" => "Aufzucht oder Pflege",
617 "type" => "type:string"
618 },
619 {
620 '@type' => "koral:doc",
621 "key" => "subTitle",
622 "match" => "match:eq",
623 "value" => "Gedichte",
624 "type" => "type:string"
625 }
626 ]
627 }
628 ]
629 },
630 {
631 '@type' => "koral:doc",
632 "key" => "pubDate",
633 "match" => "match:geq",
634 "value" => "2015-03-05",
635 "type" => "type:date"
636 }
637 ]
638 };