blob: 7d49de3476b739dc25d406e8aa21b0c1f1141c51 [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');
Akron7b9a1962020-07-02 09:52:53 +0200132 $v->optional('pipe');
Akron8ea84292018-10-24 13:41:52 +0200133 $v->optional('cutoff')->in(qw/true false/);
Akron0e1ed242018-10-11 13:22:00 +0200134
Akron32396632018-10-11 17:08:37 +0200135 $c->app->log->debug('Receive request');
136
Akron0e1ed242018-10-11 13:22:00 +0200137 # Response q=x&ql=cosmas3
138 if ($v->param('ql') && $v->param('ql') eq 'cosmas3') {
139 return $c->render(
140 status => 400,
141 json => {
142 "\@context" => "http://korap.ids-mannheim.de/ns/koral/0.3/context.jsonld",
143 "errors" => [[307,"cosmas3 is not a supported query language!"]]
144 });
145 };
146
Akron6d49c1f2018-10-11 14:22:21 +0200147 if (!$v->param('q')) {
Akron8ea84292018-10-24 13:41:52 +0200148 return $c->render(%{$c->load_response('query_no_query')});
Akron0e1ed242018-10-11 13:22:00 +0200149 };
150
Akron8ea84292018-10-24 13:41:52 +0200151 my @slug_base = ($v->param('q'));
152 push @slug_base, 'o' . $v->param('offset') if defined $v->param('offset');
153 push @slug_base, 'c' . $v->param('count') if defined $v->param('count');
154 push @slug_base, 'co' . $v->param('cutoff') if defined $v->param('cutoff');
Akroncd42a142019-07-12 18:55:37 +0200155 push @slug_base, 'cq' if defined $v->param('cq');
Akron7b9a1962020-07-02 09:52:53 +0200156 push @slug_base, 'p' . $v->param('pipe') if defined $v->param('pipe');
Akron8ea84292018-10-24 13:41:52 +0200157
Akron6d49c1f2018-10-11 14:22:21 +0200158 # Get response based on query parameter
Akron8ea84292018-10-24 13:41:52 +0200159 my $response = $c->load_response('query_' . slugify(join('_', @slug_base)));
Akron0e1ed242018-10-11 13:22:00 +0200160
161 # Check authentification
162 if (my $auth = $c->req->headers->header('Authorization')) {
Akron33f5c672019-06-24 19:40:47 +0200163
Akroncdfd9d52019-07-23 11:35:00 +0200164 $c->app->log->debug("There is an authorization header $auth");
Akron33f5c672019-06-24 19:40:47 +0200165 my $jwt;
166 if ($auth =~ /^Bearer/) {
167 # Username unknown in OAuth2
168 $response->{json}->{meta}->{authorized} = 'yes';
169 }
Akroncdfd9d52019-07-23 11:35:00 +0200170 elsif ($auth =~ /^api_token/ && ($jwt = $c->jwt_decode($auth))) {
Akron6d49c1f2018-10-11 14:22:21 +0200171 $response->{json}->{meta}->{authorized} = $jwt->{username} if $jwt->{username};
Akron0e1ed242018-10-11 13:22:00 +0200172 };
Akroncdfd9d52019-07-23 11:35:00 +0200173
174 # Code is expired
175 if ($c->expired($auth)) {
176
177 $c->app->log->debug("The access token has expired");
178
179 return $c->render(
180 status => 401,
181 json => {
182 errors => [[2003, 'Access token is expired']]
183 }
184 );
185 }
186
187 # Auth token is invalid
188 if ($auth =~ /^Bearer inv4lid/) {
189 $c->app->log->debug("The access token is invalid");
190
191 return $c->render(
192 status => 401,
193 json => {
194 errors => [[2011, 'Access token is invalid']]
195 }
196 );
197 }
Akron0e1ed242018-10-11 13:22:00 +0200198 };
199
Akron7b9a1962020-07-02 09:52:53 +0200200 if ($v->param('pipe')) {
201 $response->{json}->{meta}->{pipe} = $v->param('pipe');
202 };
203
Akron6d49c1f2018-10-11 14:22:21 +0200204 # Set page parameter
Akron0e1ed242018-10-11 13:22:00 +0200205 if ($v->param('page')) {
Akron6d49c1f2018-10-11 14:22:21 +0200206 $response->{json}->{meta}->{startIndex} = $v->param("startIndex");
Akron0e1ed242018-10-11 13:22:00 +0200207 };
208
Akron0e1ed242018-10-11 13:22:00 +0200209 # Simple search fixture
Akron32396632018-10-11 17:08:37 +0200210 $c->render(%$response);
211
212 $c->app->log->debug('Rendered result');
213
214 return 1;
Akron0e1ed242018-10-11 13:22:00 +0200215};
216
Akron80a84b22018-10-24 17:44:24 +0200217# Textinfo fixtures
Akron63d963b2019-07-05 15:35:51 +0200218get '/v1.0/corpus/:corpusId/:docId/:textId' => sub {
Akron80a84b22018-10-24 17:44:24 +0200219 my $c = shift;
220
221 my $file = join('_', (
222 'textinfo',
223 $c->stash('corpusId'),
224 $c->stash('docId'),
225 $c->stash('textId')
226 ));
227
228 my $slug = slugify($file);
229
230 # Get response based on query parameter
231 my $response = $c->load_response($slug);
232 return $c->render(%$response);
233};
234
Akron0e1ed242018-10-11 13:22:00 +0200235
Akronb80341d2018-10-15 19:46:23 +0200236# Matchinfo fixtures
Akron63d963b2019-07-05 15:35:51 +0200237get '/v1.0/corpus/:corpusId/:docId/:textId/:matchId/matchInfo' => sub {
Akronb80341d2018-10-15 19:46:23 +0200238 my $c = shift;
239
240 my $file = join('_', (
241 'matchinfo',
242 $c->stash('corpusId'),
243 $c->stash('docId'),
244 $c->stash('textId'),
245 $c->stash('matchId')
246 ));
247
Akronb8d0b402018-10-18 23:51:52 +0200248 my $slug = slugify($file);
249
Akronb80341d2018-10-15 19:46:23 +0200250 # Get response based on query parameter
Akronb8d0b402018-10-18 23:51:52 +0200251 my $response = $c->load_response($slug);
Akronb80341d2018-10-15 19:46:23 +0200252 return $c->render(%$response);
253};
254
Akron0e1ed242018-10-11 13:22:00 +0200255
Akronbe61f4c2018-10-20 00:52:58 +0200256# Statistics endpoint
Akron63d963b2019-07-05 15:35:51 +0200257get '/v1.0/statistics' => sub {
Akronbe61f4c2018-10-20 00:52:58 +0200258 my $c = shift;
259 my $v = $c->validation;
Akron5fa61e92019-07-15 11:56:11 +0200260 $v->optional('cq');
Akronbe61f4c2018-10-20 00:52:58 +0200261
262 my @list = 'corpusinfo';
Akron5fa61e92019-07-15 11:56:11 +0200263 if ($v->param('cq')) {
264 push @list, $v->param('cq');
Akronbe61f4c2018-10-20 00:52:58 +0200265 };
266 my $slug = slugify(join('_', @list));
267
268 # Get response based on query parameter
269 my $response = $c->load_response($slug);
270 return $c->render(%$response);
271};
272
Akron0e1ed242018-10-11 13:22:00 +0200273############
274# Auth API #
275############
276
277# Request API token
Akron63d963b2019-07-05 15:35:51 +0200278get '/v1.0/auth/logout' => sub {
Akron0e1ed242018-10-11 13:22:00 +0200279 my $c = shift;
280
281 if (my $auth = $c->req->headers->header('Authorization')) {
Akroncdfd9d52019-07-23 11:35:00 +0200282
283 if ($auth =~ /^Bearer/) {
284 $c->app->log->debug('Server-Logout: ' . $auth);
285 return $c->render(json => { msg => [[0, 'Fine!']]});
286 }
287
288 elsif (my $jwt = $c->jwt_decode($auth)) {
Akron0e1ed242018-10-11 13:22:00 +0200289 my $user = $jwt->{username} if $jwt->{username};
290
291 $c->app->log->debug('Server-Logout: ' . $user);
292 return $c->render(json => { msg => [[0, 'Fine!']]});
293 };
294 };
295
296 return $c->render(status => 400, json => { error => [[0, 'No!']]});
297};
298
299
300# Request API token
Akron63d963b2019-07-05 15:35:51 +0200301get '/v1.0/auth/apiToken' => sub {
Akron0e1ed242018-10-11 13:22:00 +0200302 my $c = shift;
303
304 # Get auth header
305 my $auth = $c->req->headers->authorization;
306
307 # Authorization missing or not basic
308 if (!$auth || $auth !~ s/\s*Basic\s+//gi) {
309 return $c->render(
310 json => {
311 error => [[2, 'x']]
312 }
313 );
314 };
315
316 # Decode header
317 my ($username, $pwd) = @{b($auth)->b64_decode->split(':')->to_array};
318
319 # the password is 'pass'
320 if ($pwd) {
321
322 # the password is 'pass'
323 if ($pwd eq 'pass') {
324
325 # Render info with token
326 my $jwt = $c->jwt_encode(username => $username);
327
328 # Render in the Kustvakt fashion:
329 return $c->render(
330 format => 'html',
331 text => encode_json({
332 %{$jwt->claims},
333 expires => $jwt->expires,
334 token => $jwt->encode,
335 token_type => 'api_token'
336 })
337 );
Akron3d673062019-01-29 15:54:16 +0100338 }
339
340 elsif ($pwd eq 'ldaperr') {
341 return $c->render(
342 format => 'html',
343 status => 401,
344 json => {
345 "errors" => [[2022,"LDAP Authentication failed due to unknown user or password!"]]
346 }
347 );
Akron0e1ed242018-10-11 13:22:00 +0200348 };
349
350 return $c->render(
351 json => {
352 error => [[2004, undef]]
353 }
354 );
355 };
356
357 return $c->render(
358 json => {
359 error => [[2004, undef]]
360 }
361 );
362};
363
Akron33f5c672019-06-24 19:40:47 +0200364
365# Request API token
Akron63d963b2019-07-05 15:35:51 +0200366post '/v1.0/oauth2/token' => sub {
Akron33f5c672019-06-24 19:40:47 +0200367 my $c = shift;
368
Akron63d963b2019-07-05 15:35:51 +0200369 my $grant_type = $c->param('grant_type') // 'undefined';
370
371 if ($grant_type eq 'password') {
Akron33f5c672019-06-24 19:40:47 +0200372
Akron8bbbecf2019-07-01 18:57:30 +0200373 # Check for wrong client id
374 if ($c->param('client_id') ne '2') {
375 return $c->render(
376 json => {
377 "error_description" => "Unknown client with " . $_->{client_id},
378 "error" => "invalid_client"
379 },
380 status => 401
381 );
382 }
Akron33f5c672019-06-24 19:40:47 +0200383
Akron8bbbecf2019-07-01 18:57:30 +0200384 # Check for wrong client secret
385 elsif ($c->param('client_secret') ne 'k414m4r-s3cr3t') {
386 return $c->render(
387 json => {
388 "error_description" => "Invalid client credentials",
389 "error" => "invalid_client"
390 },
391 status => 401
392 );
393 }
Akron33f5c672019-06-24 19:40:47 +0200394
Akron8bbbecf2019-07-01 18:57:30 +0200395 # Check for wrong user name
396 elsif ($c->param('username') ne 'test') {
397 return $c->render(json => {
398 error => [[2004, undef]]
399 });
400 }
401
402 # Check for ldap error
403 elsif ($c->param('password') eq 'ldaperr') {
404 return $c->render(
405 format => 'html',
406 status => 401,
407 json => {
408 "errors" => [
409 [
410 2022,
411 "LDAP Authentication failed due to unknown user or password!"
412 ]
413 ]
414 }
415 );
416 }
417
418 # Check for wrong password
419 elsif ($c->param('password') ne 'pass') {
420 return $c->render(json => {
421 format => 'html',
422 status => 401,
Akron33f5c672019-06-24 19:40:47 +0200423 "errors" => [[2022,"LDAP Authentication failed due to unknown user or password!"]]
Akron8bbbecf2019-07-01 18:57:30 +0200424 });
425 }
426
427 # Return fine access
428 return $c->render(
429 json => {
Akroncdfd9d52019-07-23 11:35:00 +0200430 "access_token" => $c->get_token('access_token'),
431 "refresh_token" => $c->get_token('refresh_token'),
Akron8bbbecf2019-07-01 18:57:30 +0200432 "scope" => "all",
433 "token_type" => "Bearer",
434 "expires_in" => 86400
435 });
436 }
437
438 # Refresh token
Akron63d963b2019-07-05 15:35:51 +0200439 elsif ($grant_type eq 'refresh_token') {
Akroncdfd9d52019-07-23 11:35:00 +0200440
441 if ($c->param('refresh_token') eq 'inv4lid') {
442 return $c->render(
443 status => 400,
444 json => {
445 "error_description" => "Refresh token is expired",
446 "error" => "invalid_grant"
447 }
448 );
449 };
450
451 $c->app->log->debug("Refresh the token in the mock server!");
452
Akron8bbbecf2019-07-01 18:57:30 +0200453 return $c->render(
454 status => 200,
455 json => {
Akroncdfd9d52019-07-23 11:35:00 +0200456 "access_token" => $c->get_token("access_token_2"),
457 "refresh_token" => $c->get_token("refresh_token_2"),
Akron8bbbecf2019-07-01 18:57:30 +0200458 "token_type" => "Bearer",
459 "expires_in" => 86400
Akron33f5c672019-06-24 19:40:47 +0200460 }
461 );
462 }
463
Akron8bbbecf2019-07-01 18:57:30 +0200464 # Unknown token grant
465 else {
466 return $c->render(
Akron63d963b2019-07-05 15:35:51 +0200467 status => 400,
Akron8bbbecf2019-07-01 18:57:30 +0200468 json => {
469 "errors" => [
470 [
Akron63d963b2019-07-05 15:35:51 +0200471 0, "Grant Type unknown", $grant_type
Akron8bbbecf2019-07-01 18:57:30 +0200472 ]
473 ]
474 }
475 )
Akron33f5c672019-06-24 19:40:47 +0200476 }
Akron33f5c672019-06-24 19:40:47 +0200477};
478
Akron4cefe1f2019-09-04 10:11:28 +0200479# Revoke API token
480post '/v1.0/oauth2/revoke' => sub {
481 my $c = shift;
482
483 my $refresh_token = $c->param('token');
484
485 if ($c->param('client_secret') ne 'k414m4r-s3cr3t') {
486 return $c->render(
487 json => {
488 "error_description" => "Invalid client credentials",
489 "error" => "invalid_client"
490 },
491 status => 401
492 );
493 };
494
495 return $c->render(
496 text => ''
497 )
498};
Akron33f5c672019-06-24 19:40:47 +0200499
Akron59992122019-10-29 11:28:45 +0100500# Register a client
501post '/v1.0/oauth2/client/register' => sub {
502 my $c = shift;
503 my $json = $c->req->json;
504
505 my $name = $json->{name};
Akron1a9d5be2020-03-19 17:28:33 +0100506 my $desc = $json->{description};
Akron59992122019-10-29 11:28:45 +0100507 my $type = $json->{type};
508 my $url = $json->{url};
509 my $redirect_url = $json->{redirectURI};
510
Akron1a9d5be2020-03-19 17:28:33 +0100511 my $list = $c->app->defaults('oauth.client_list');
512
513 push @$list, {
514 "clientId" => $tokens{new_client_id},
515 "clientName" => $name,
516 "description" => $desc,
517 "url" => $url
518 };
519
Akron59992122019-10-29 11:28:45 +0100520 # Confidential server application
521 if ($type eq 'CONFIDENTIAL') {
522 return $c->render(json => {
523 client_id => $tokens{new_client_id},
524 client_secret => $tokens{new_client_secret}
525 });
526 };
527
528 # Desktop application
529 return $c->render(json => {
530 client_id => $tokens{new_client_id}
531 });
532};
533
Akron33f5c672019-06-24 19:40:47 +0200534
Akron0f1b93b2020-03-17 11:37:19 +0100535# Register a client
536post '/v1.0/oauth2/client/list' => sub {
537 my $c = shift;
538
539 # $c->param('client_secret');
Akron1a9d5be2020-03-19 17:28:33 +0100540
541 # Is empty [] when nothing registered
542
Akron0f1b93b2020-03-17 11:37:19 +0100543 return $c->render(
Akron1a9d5be2020-03-19 17:28:33 +0100544 json => $c->stash('oauth.client_list'),
545 status => 200
546 );
547};
548
549del '/v1.0/oauth2/client/deregister/:client_id' => sub {
550 my $c = shift;
551 my $client_id = $c->stash('client_id');
552
553 my $list = $c->app->defaults('oauth.client_list');
554
555 my $break = -1;
556 for (my $i = 0; $i < @$list; $i++) {
557 if ($list->[$i]->{clientId} eq $client_id) {
558 $break = $i;
559 last;
560 };
561 };
562
563 if ($break != -1) {
564 splice @$list, $break, 1;
565 }
566
567 else {
568 return $c->render(
569 json => {
570 error_description => "Unknown client with $client_id.",
571 error => "invalid_client"
Akron0f1b93b2020-03-17 11:37:19 +0100572 },
Akron1a9d5be2020-03-19 17:28:33 +0100573 status => 401
574 );
575 };
576
577 return $c->render(
578 json => $c->stash('oauth.client_list'),
Akron0f1b93b2020-03-17 11:37:19 +0100579 status => 200
580 );
581};
582
583
584
Akron0e1ed242018-10-11 13:22:00 +0200585app->start;
586
587
588__END__
589
590
591 # Temporary:
592 my $collection_query = {
593 '@type' => "koral:docGroup",
594 "operation" => "operation:or",
595 "operands" => [
596 {
597 '@type' => "koral:docGroup",
598 "operation" => "operation:and",
599 "operands" => [
600 {
601 '@type' => "koral:doc",
602 "key" => "title",
603 "match" => "match:eq",
604 "value" => "Der Birnbaum",
605 "type" => "type:string"
606 },
607 {
608 '@type' => "koral:doc",
609 "key" => "pubPlace",
610 "match" => "match:eq",
611 "value" => "Mannheim",
612 "type" => "type:string"
613 },
614 {
615 '@type' => "koral:docGroup",
616 "operation" => "operation:or",
617 "operands" => [
618 {
619 '@type' => "koral:doc",
620 "key" => "subTitle",
621 "match" => "match:eq",
622 "value" => "Aufzucht oder Pflege",
623 "type" => "type:string"
624 },
625 {
626 '@type' => "koral:doc",
627 "key" => "subTitle",
628 "match" => "match:eq",
629 "value" => "Gedichte",
630 "type" => "type:string"
631 }
632 ]
633 }
634 ]
635 },
636 {
637 '@type' => "koral:doc",
638 "key" => "pubDate",
639 "match" => "match:geq",
640 "value" => "2015-03-05",
641 "type" => "type:date"
642 }
643 ]
644 };