blob: 21192f0e00636af36d7b74e4225bde290d1c58d7 [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
98
Akron0e1ed242018-10-11 13:22:00 +020099# Base page
Akron63d963b2019-07-05 15:35:51 +0200100get '/v1.0/' => sub {
Akron6d49c1f2018-10-11 14:22:21 +0200101 shift->render(text => 'Fake server available');
Akron0e1ed242018-10-11 13:22:00 +0200102};
103
Akron32396632018-10-11 17:08:37 +0200104
Akrond00b4272020-02-05 17:00:33 +0100105get '/v1.0/redirect-target-a' => sub {
106 shift->render(text => 'Redirect Target!');
107} => 'redirect-target';
108
109
110# Base page
111get '/v1.0/redirect' => sub {
112 my $c = shift;
113 $c->res->code(308);
114 $c->res->headers->location($c->url_for('redirect-target')->to_abs);
115 return $c->render(text => '');
116};
117
118
Akron0e1ed242018-10-11 13:22:00 +0200119# Search fixtures
Akron63d963b2019-07-05 15:35:51 +0200120get '/v1.0/search' => sub {
Akron0e1ed242018-10-11 13:22:00 +0200121 my $c = shift;
122 my $v = $c->validation;
123 $v->optional('q');
124 $v->optional('page');
125 $v->optional('ql');
Akroncd42a142019-07-12 18:55:37 +0200126 $v->optional('cq');
Akron0e1ed242018-10-11 13:22:00 +0200127 $v->optional('count');
128 $v->optional('context');
Akron8ea84292018-10-24 13:41:52 +0200129 $v->optional('offset');
130 $v->optional('cutoff')->in(qw/true false/);
Akron0e1ed242018-10-11 13:22:00 +0200131
Akron32396632018-10-11 17:08:37 +0200132 $c->app->log->debug('Receive request');
133
Akron0e1ed242018-10-11 13:22:00 +0200134 # Response q=x&ql=cosmas3
135 if ($v->param('ql') && $v->param('ql') eq 'cosmas3') {
136 return $c->render(
137 status => 400,
138 json => {
139 "\@context" => "http://korap.ids-mannheim.de/ns/koral/0.3/context.jsonld",
140 "errors" => [[307,"cosmas3 is not a supported query language!"]]
141 });
142 };
143
Akron6d49c1f2018-10-11 14:22:21 +0200144 if (!$v->param('q')) {
Akron8ea84292018-10-24 13:41:52 +0200145 return $c->render(%{$c->load_response('query_no_query')});
Akron0e1ed242018-10-11 13:22:00 +0200146 };
147
Akron8ea84292018-10-24 13:41:52 +0200148 my @slug_base = ($v->param('q'));
149 push @slug_base, 'o' . $v->param('offset') if defined $v->param('offset');
150 push @slug_base, 'c' . $v->param('count') if defined $v->param('count');
151 push @slug_base, 'co' . $v->param('cutoff') if defined $v->param('cutoff');
Akroncd42a142019-07-12 18:55:37 +0200152 push @slug_base, 'cq' if defined $v->param('cq');
Akron8ea84292018-10-24 13:41:52 +0200153
Akron6d49c1f2018-10-11 14:22:21 +0200154 # Get response based on query parameter
Akron8ea84292018-10-24 13:41:52 +0200155 my $response = $c->load_response('query_' . slugify(join('_', @slug_base)));
Akron0e1ed242018-10-11 13:22:00 +0200156
157 # Check authentification
158 if (my $auth = $c->req->headers->header('Authorization')) {
Akron33f5c672019-06-24 19:40:47 +0200159
Akroncdfd9d52019-07-23 11:35:00 +0200160 $c->app->log->debug("There is an authorization header $auth");
Akron33f5c672019-06-24 19:40:47 +0200161 my $jwt;
162 if ($auth =~ /^Bearer/) {
163 # Username unknown in OAuth2
164 $response->{json}->{meta}->{authorized} = 'yes';
165 }
Akroncdfd9d52019-07-23 11:35:00 +0200166 elsif ($auth =~ /^api_token/ && ($jwt = $c->jwt_decode($auth))) {
Akron6d49c1f2018-10-11 14:22:21 +0200167 $response->{json}->{meta}->{authorized} = $jwt->{username} if $jwt->{username};
Akron0e1ed242018-10-11 13:22:00 +0200168 };
Akroncdfd9d52019-07-23 11:35:00 +0200169
170 # Code is expired
171 if ($c->expired($auth)) {
172
173 $c->app->log->debug("The access token has expired");
174
175 return $c->render(
176 status => 401,
177 json => {
178 errors => [[2003, 'Access token is expired']]
179 }
180 );
181 }
182
183 # Auth token is invalid
184 if ($auth =~ /^Bearer inv4lid/) {
185 $c->app->log->debug("The access token is invalid");
186
187 return $c->render(
188 status => 401,
189 json => {
190 errors => [[2011, 'Access token is invalid']]
191 }
192 );
193 }
Akron0e1ed242018-10-11 13:22:00 +0200194 };
195
Akron6d49c1f2018-10-11 14:22:21 +0200196 # Set page parameter
Akron0e1ed242018-10-11 13:22:00 +0200197 if ($v->param('page')) {
Akron6d49c1f2018-10-11 14:22:21 +0200198 $response->{json}->{meta}->{startIndex} = $v->param("startIndex");
Akron0e1ed242018-10-11 13:22:00 +0200199 };
200
Akron0e1ed242018-10-11 13:22:00 +0200201 # Simple search fixture
Akron32396632018-10-11 17:08:37 +0200202 $c->render(%$response);
203
204 $c->app->log->debug('Rendered result');
205
206 return 1;
Akron0e1ed242018-10-11 13:22:00 +0200207};
208
Akron80a84b22018-10-24 17:44:24 +0200209# Textinfo fixtures
Akron63d963b2019-07-05 15:35:51 +0200210get '/v1.0/corpus/:corpusId/:docId/:textId' => sub {
Akron80a84b22018-10-24 17:44:24 +0200211 my $c = shift;
212
213 my $file = join('_', (
214 'textinfo',
215 $c->stash('corpusId'),
216 $c->stash('docId'),
217 $c->stash('textId')
218 ));
219
220 my $slug = slugify($file);
221
222 # Get response based on query parameter
223 my $response = $c->load_response($slug);
224 return $c->render(%$response);
225};
226
Akron0e1ed242018-10-11 13:22:00 +0200227
Akronb80341d2018-10-15 19:46:23 +0200228# Matchinfo fixtures
Akron63d963b2019-07-05 15:35:51 +0200229get '/v1.0/corpus/:corpusId/:docId/:textId/:matchId/matchInfo' => sub {
Akronb80341d2018-10-15 19:46:23 +0200230 my $c = shift;
231
232 my $file = join('_', (
233 'matchinfo',
234 $c->stash('corpusId'),
235 $c->stash('docId'),
236 $c->stash('textId'),
237 $c->stash('matchId')
238 ));
239
Akronb8d0b402018-10-18 23:51:52 +0200240 my $slug = slugify($file);
241
Akronb80341d2018-10-15 19:46:23 +0200242 # Get response based on query parameter
Akronb8d0b402018-10-18 23:51:52 +0200243 my $response = $c->load_response($slug);
Akronb80341d2018-10-15 19:46:23 +0200244 return $c->render(%$response);
245};
246
Akron0e1ed242018-10-11 13:22:00 +0200247
Akronbe61f4c2018-10-20 00:52:58 +0200248# Statistics endpoint
Akron63d963b2019-07-05 15:35:51 +0200249get '/v1.0/statistics' => sub {
Akronbe61f4c2018-10-20 00:52:58 +0200250 my $c = shift;
251 my $v = $c->validation;
Akron5fa61e92019-07-15 11:56:11 +0200252 $v->optional('cq');
Akronbe61f4c2018-10-20 00:52:58 +0200253
254 my @list = 'corpusinfo';
Akron5fa61e92019-07-15 11:56:11 +0200255 if ($v->param('cq')) {
256 push @list, $v->param('cq');
Akronbe61f4c2018-10-20 00:52:58 +0200257 };
258 my $slug = slugify(join('_', @list));
259
260 # Get response based on query parameter
261 my $response = $c->load_response($slug);
262 return $c->render(%$response);
263};
264
Akron0e1ed242018-10-11 13:22:00 +0200265############
266# Auth API #
267############
268
269# Request API token
Akron63d963b2019-07-05 15:35:51 +0200270get '/v1.0/auth/logout' => sub {
Akron0e1ed242018-10-11 13:22:00 +0200271 my $c = shift;
272
273 if (my $auth = $c->req->headers->header('Authorization')) {
Akroncdfd9d52019-07-23 11:35:00 +0200274
275 if ($auth =~ /^Bearer/) {
276 $c->app->log->debug('Server-Logout: ' . $auth);
277 return $c->render(json => { msg => [[0, 'Fine!']]});
278 }
279
280 elsif (my $jwt = $c->jwt_decode($auth)) {
Akron0e1ed242018-10-11 13:22:00 +0200281 my $user = $jwt->{username} if $jwt->{username};
282
283 $c->app->log->debug('Server-Logout: ' . $user);
284 return $c->render(json => { msg => [[0, 'Fine!']]});
285 };
286 };
287
288 return $c->render(status => 400, json => { error => [[0, 'No!']]});
289};
290
291
292# Request API token
Akron63d963b2019-07-05 15:35:51 +0200293get '/v1.0/auth/apiToken' => sub {
Akron0e1ed242018-10-11 13:22:00 +0200294 my $c = shift;
295
296 # Get auth header
297 my $auth = $c->req->headers->authorization;
298
299 # Authorization missing or not basic
300 if (!$auth || $auth !~ s/\s*Basic\s+//gi) {
301 return $c->render(
302 json => {
303 error => [[2, 'x']]
304 }
305 );
306 };
307
308 # Decode header
309 my ($username, $pwd) = @{b($auth)->b64_decode->split(':')->to_array};
310
311 # the password is 'pass'
312 if ($pwd) {
313
314 # the password is 'pass'
315 if ($pwd eq 'pass') {
316
317 # Render info with token
318 my $jwt = $c->jwt_encode(username => $username);
319
320 # Render in the Kustvakt fashion:
321 return $c->render(
322 format => 'html',
323 text => encode_json({
324 %{$jwt->claims},
325 expires => $jwt->expires,
326 token => $jwt->encode,
327 token_type => 'api_token'
328 })
329 );
Akron3d673062019-01-29 15:54:16 +0100330 }
331
332 elsif ($pwd eq 'ldaperr') {
333 return $c->render(
334 format => 'html',
335 status => 401,
336 json => {
337 "errors" => [[2022,"LDAP Authentication failed due to unknown user or password!"]]
338 }
339 );
Akron0e1ed242018-10-11 13:22:00 +0200340 };
341
342 return $c->render(
343 json => {
344 error => [[2004, undef]]
345 }
346 );
347 };
348
349 return $c->render(
350 json => {
351 error => [[2004, undef]]
352 }
353 );
354};
355
Akron33f5c672019-06-24 19:40:47 +0200356
357# Request API token
Akron63d963b2019-07-05 15:35:51 +0200358post '/v1.0/oauth2/token' => sub {
Akron33f5c672019-06-24 19:40:47 +0200359 my $c = shift;
360
Akron63d963b2019-07-05 15:35:51 +0200361 my $grant_type = $c->param('grant_type') // 'undefined';
362
363 if ($grant_type eq 'password') {
Akron33f5c672019-06-24 19:40:47 +0200364
Akron8bbbecf2019-07-01 18:57:30 +0200365 # Check for wrong client id
366 if ($c->param('client_id') ne '2') {
367 return $c->render(
368 json => {
369 "error_description" => "Unknown client with " . $_->{client_id},
370 "error" => "invalid_client"
371 },
372 status => 401
373 );
374 }
Akron33f5c672019-06-24 19:40:47 +0200375
Akron8bbbecf2019-07-01 18:57:30 +0200376 # Check for wrong client secret
377 elsif ($c->param('client_secret') ne 'k414m4r-s3cr3t') {
378 return $c->render(
379 json => {
380 "error_description" => "Invalid client credentials",
381 "error" => "invalid_client"
382 },
383 status => 401
384 );
385 }
Akron33f5c672019-06-24 19:40:47 +0200386
Akron8bbbecf2019-07-01 18:57:30 +0200387 # Check for wrong user name
388 elsif ($c->param('username') ne 'test') {
389 return $c->render(json => {
390 error => [[2004, undef]]
391 });
392 }
393
394 # Check for ldap error
395 elsif ($c->param('password') eq 'ldaperr') {
396 return $c->render(
397 format => 'html',
398 status => 401,
399 json => {
400 "errors" => [
401 [
402 2022,
403 "LDAP Authentication failed due to unknown user or password!"
404 ]
405 ]
406 }
407 );
408 }
409
410 # Check for wrong password
411 elsif ($c->param('password') ne 'pass') {
412 return $c->render(json => {
413 format => 'html',
414 status => 401,
Akron33f5c672019-06-24 19:40:47 +0200415 "errors" => [[2022,"LDAP Authentication failed due to unknown user or password!"]]
Akron8bbbecf2019-07-01 18:57:30 +0200416 });
417 }
418
419 # Return fine access
420 return $c->render(
421 json => {
Akroncdfd9d52019-07-23 11:35:00 +0200422 "access_token" => $c->get_token('access_token'),
423 "refresh_token" => $c->get_token('refresh_token'),
Akron8bbbecf2019-07-01 18:57:30 +0200424 "scope" => "all",
425 "token_type" => "Bearer",
426 "expires_in" => 86400
427 });
428 }
429
430 # Refresh token
Akron63d963b2019-07-05 15:35:51 +0200431 elsif ($grant_type eq 'refresh_token') {
Akroncdfd9d52019-07-23 11:35:00 +0200432
433 if ($c->param('refresh_token') eq 'inv4lid') {
434 return $c->render(
435 status => 400,
436 json => {
437 "error_description" => "Refresh token is expired",
438 "error" => "invalid_grant"
439 }
440 );
441 };
442
443 $c->app->log->debug("Refresh the token in the mock server!");
444
Akron8bbbecf2019-07-01 18:57:30 +0200445 return $c->render(
446 status => 200,
447 json => {
Akroncdfd9d52019-07-23 11:35:00 +0200448 "access_token" => $c->get_token("access_token_2"),
449 "refresh_token" => $c->get_token("refresh_token_2"),
Akron8bbbecf2019-07-01 18:57:30 +0200450 "token_type" => "Bearer",
451 "expires_in" => 86400
Akron33f5c672019-06-24 19:40:47 +0200452 }
453 );
454 }
455
Akron8bbbecf2019-07-01 18:57:30 +0200456 # Unknown token grant
457 else {
458 return $c->render(
Akron63d963b2019-07-05 15:35:51 +0200459 status => 400,
Akron8bbbecf2019-07-01 18:57:30 +0200460 json => {
461 "errors" => [
462 [
Akron63d963b2019-07-05 15:35:51 +0200463 0, "Grant Type unknown", $grant_type
Akron8bbbecf2019-07-01 18:57:30 +0200464 ]
465 ]
466 }
467 )
Akron33f5c672019-06-24 19:40:47 +0200468 }
Akron33f5c672019-06-24 19:40:47 +0200469};
470
Akron4cefe1f2019-09-04 10:11:28 +0200471# Revoke API token
472post '/v1.0/oauth2/revoke' => sub {
473 my $c = shift;
474
475 my $refresh_token = $c->param('token');
476
477 if ($c->param('client_secret') ne 'k414m4r-s3cr3t') {
478 return $c->render(
479 json => {
480 "error_description" => "Invalid client credentials",
481 "error" => "invalid_client"
482 },
483 status => 401
484 );
485 };
486
487 return $c->render(
488 text => ''
489 )
490};
Akron33f5c672019-06-24 19:40:47 +0200491
Akron59992122019-10-29 11:28:45 +0100492# Register a client
493post '/v1.0/oauth2/client/register' => sub {
494 my $c = shift;
495 my $json = $c->req->json;
496
497 my $name = $json->{name};
498 my $desc = $json->{desc};
499 my $type = $json->{type};
500 my $url = $json->{url};
501 my $redirect_url = $json->{redirectURI};
502
503 # Confidential server application
504 if ($type eq 'CONFIDENTIAL') {
505 return $c->render(json => {
506 client_id => $tokens{new_client_id},
507 client_secret => $tokens{new_client_secret}
508 });
509 };
510
511 # Desktop application
512 return $c->render(json => {
513 client_id => $tokens{new_client_id}
514 });
515};
516
Akron33f5c672019-06-24 19:40:47 +0200517
Akron0e1ed242018-10-11 13:22:00 +0200518app->start;
519
520
521__END__
522
523
524 # Temporary:
525 my $collection_query = {
526 '@type' => "koral:docGroup",
527 "operation" => "operation:or",
528 "operands" => [
529 {
530 '@type' => "koral:docGroup",
531 "operation" => "operation:and",
532 "operands" => [
533 {
534 '@type' => "koral:doc",
535 "key" => "title",
536 "match" => "match:eq",
537 "value" => "Der Birnbaum",
538 "type" => "type:string"
539 },
540 {
541 '@type' => "koral:doc",
542 "key" => "pubPlace",
543 "match" => "match:eq",
544 "value" => "Mannheim",
545 "type" => "type:string"
546 },
547 {
548 '@type' => "koral:docGroup",
549 "operation" => "operation:or",
550 "operands" => [
551 {
552 '@type' => "koral:doc",
553 "key" => "subTitle",
554 "match" => "match:eq",
555 "value" => "Aufzucht oder Pflege",
556 "type" => "type:string"
557 },
558 {
559 '@type' => "koral:doc",
560 "key" => "subTitle",
561 "match" => "match:eq",
562 "value" => "Gedichte",
563 "type" => "type:string"
564 }
565 ]
566 }
567 ]
568 },
569 {
570 '@type' => "koral:doc",
571 "key" => "pubDate",
572 "match" => "match:geq",
573 "value" => "2015-03-05",
574 "type" => "type:date"
575 }
576 ]
577 };