blob: a1f05960598e2dc119f1af3002a98f8f597cab24 [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 = (
18 "access_token" => "4dcf8784ccfd26fac9bdb82778fe60e2",
19 "refresh_token" => "hlWci75xb8atDiq3924NUSvOdtAh7Nlf9z",
20 "access_token_2" => "abcde",
21 "refresh_token_2" => "fghijk"
22);
23
24helper get_token => sub {
25 my ($c, $token) = @_;
26 return $tokens{$token}
27};
28
Akron33f5c672019-06-24 19:40:47 +020029# Legacy:
Akron0e1ed242018-10-11 13:22:00 +020030helper jwt_encode => sub {
31 shift;
32 return Mojo::JWT->new(
33 secret => $secret,
34 token_type => 'api_token',
35 expires => time + (3 * 34 * 60 * 60),
36 claims => { @_ }
37 );
38};
39
Akron33f5c672019-06-24 19:40:47 +020040# Legacy;
Akron0e1ed242018-10-11 13:22:00 +020041helper jwt_decode => sub {
42 my ($c, $auth) = @_;
43 $auth =~ s/\s*api_token\s+//;
44 return Mojo::JWT->new(secret => $secret)->decode($auth);
45};
46
Akroncdfd9d52019-07-23 11:35:00 +020047# Expiration helper
48helper expired => sub {
49 my ($c, $auth, $set) = @_;
50
51
52 $auth =~ s/^[^ ]+? //;
53 if ($set) {
54 $c->app->log->debug("Set $auth for expiration");
55 $c->app->defaults('auth_' . $auth => 1);
56 return 1;
57 };
58
59 $c->app->log->debug("Check $auth for expiration: " . (
60 $c->app->defaults('auth_' . $auth) // '0'
61 ));
62
63 return $c->app->defaults('auth_' . $auth);
64};
Akron0e1ed242018-10-11 13:22:00 +020065
Akron6d49c1f2018-10-11 14:22:21 +020066# Load fixture responses
67helper 'load_response' => sub {
68 my $c = shift;
69 my $q_name = shift;
70 my $file = $fixture_path->child("response_$q_name.json");
Akron8ea84292018-10-24 13:41:52 +020071 $c->app->log->debug("Load response from $file");
72
Akron6d49c1f2018-10-11 14:22:21 +020073 unless (-f $file) {
74 return {
75 status => 500,
76 json => {
77 errors => [[0, 'Unable to load query response from ' . $file]]
78 }
79 }
80 };
Akron8ea84292018-10-24 13:41:52 +020081
Akron6d49c1f2018-10-11 14:22:21 +020082 my $response = $file->slurp;
Akrona3c353c2019-02-14 23:50:00 +010083 my $decode = decode_json($response);
84 unless ($decode) {
85 return {
86 status => 500,
87 json => {
88 errors => [[0, 'Unable to parse JSON']]
89 }
90 }
91 };
92
93 return $decode;
Akron6d49c1f2018-10-11 14:22:21 +020094};
95
96
Akron0e1ed242018-10-11 13:22:00 +020097# Base page
Akron63d963b2019-07-05 15:35:51 +020098get '/v1.0/' => sub {
Akron6d49c1f2018-10-11 14:22:21 +020099 shift->render(text => 'Fake server available');
Akron0e1ed242018-10-11 13:22:00 +0200100};
101
Akron32396632018-10-11 17:08:37 +0200102
Akrond00b4272020-02-05 17:00:33 +0100103get '/v1.0/redirect-target-a' => sub {
104 shift->render(text => 'Redirect Target!');
105} => 'redirect-target';
106
107
108# Base page
109get '/v1.0/redirect' => sub {
110 my $c = shift;
111 $c->res->code(308);
112 $c->res->headers->location($c->url_for('redirect-target')->to_abs);
113 return $c->render(text => '');
114};
115
116
Akron0e1ed242018-10-11 13:22:00 +0200117# Search fixtures
Akron63d963b2019-07-05 15:35:51 +0200118get '/v1.0/search' => sub {
Akron0e1ed242018-10-11 13:22:00 +0200119 my $c = shift;
120 my $v = $c->validation;
121 $v->optional('q');
122 $v->optional('page');
123 $v->optional('ql');
Akroncd42a142019-07-12 18:55:37 +0200124 $v->optional('cq');
Akron0e1ed242018-10-11 13:22:00 +0200125 $v->optional('count');
126 $v->optional('context');
Akron8ea84292018-10-24 13:41:52 +0200127 $v->optional('offset');
128 $v->optional('cutoff')->in(qw/true false/);
Akron0e1ed242018-10-11 13:22:00 +0200129
Akron32396632018-10-11 17:08:37 +0200130 $c->app->log->debug('Receive request');
131
Akron0e1ed242018-10-11 13:22:00 +0200132 # Response q=x&ql=cosmas3
133 if ($v->param('ql') && $v->param('ql') eq 'cosmas3') {
134 return $c->render(
135 status => 400,
136 json => {
137 "\@context" => "http://korap.ids-mannheim.de/ns/koral/0.3/context.jsonld",
138 "errors" => [[307,"cosmas3 is not a supported query language!"]]
139 });
140 };
141
Akron6d49c1f2018-10-11 14:22:21 +0200142 if (!$v->param('q')) {
Akron8ea84292018-10-24 13:41:52 +0200143 return $c->render(%{$c->load_response('query_no_query')});
Akron0e1ed242018-10-11 13:22:00 +0200144 };
145
Akron8ea84292018-10-24 13:41:52 +0200146 my @slug_base = ($v->param('q'));
147 push @slug_base, 'o' . $v->param('offset') if defined $v->param('offset');
148 push @slug_base, 'c' . $v->param('count') if defined $v->param('count');
149 push @slug_base, 'co' . $v->param('cutoff') if defined $v->param('cutoff');
Akroncd42a142019-07-12 18:55:37 +0200150 push @slug_base, 'cq' if defined $v->param('cq');
Akron8ea84292018-10-24 13:41:52 +0200151
Akron6d49c1f2018-10-11 14:22:21 +0200152 # Get response based on query parameter
Akron8ea84292018-10-24 13:41:52 +0200153 my $response = $c->load_response('query_' . slugify(join('_', @slug_base)));
Akron0e1ed242018-10-11 13:22:00 +0200154
155 # Check authentification
156 if (my $auth = $c->req->headers->header('Authorization')) {
Akron33f5c672019-06-24 19:40:47 +0200157
Akroncdfd9d52019-07-23 11:35:00 +0200158 $c->app->log->debug("There is an authorization header $auth");
Akron33f5c672019-06-24 19:40:47 +0200159 my $jwt;
160 if ($auth =~ /^Bearer/) {
161 # Username unknown in OAuth2
162 $response->{json}->{meta}->{authorized} = 'yes';
163 }
Akroncdfd9d52019-07-23 11:35:00 +0200164 elsif ($auth =~ /^api_token/ && ($jwt = $c->jwt_decode($auth))) {
Akron6d49c1f2018-10-11 14:22:21 +0200165 $response->{json}->{meta}->{authorized} = $jwt->{username} if $jwt->{username};
Akron0e1ed242018-10-11 13:22:00 +0200166 };
Akroncdfd9d52019-07-23 11:35:00 +0200167
168 # Code is expired
169 if ($c->expired($auth)) {
170
171 $c->app->log->debug("The access token has expired");
172
173 return $c->render(
174 status => 401,
175 json => {
176 errors => [[2003, 'Access token is expired']]
177 }
178 );
179 }
180
181 # Auth token is invalid
182 if ($auth =~ /^Bearer inv4lid/) {
183 $c->app->log->debug("The access token is invalid");
184
185 return $c->render(
186 status => 401,
187 json => {
188 errors => [[2011, 'Access token is invalid']]
189 }
190 );
191 }
Akron0e1ed242018-10-11 13:22:00 +0200192 };
193
Akron6d49c1f2018-10-11 14:22:21 +0200194 # Set page parameter
Akron0e1ed242018-10-11 13:22:00 +0200195 if ($v->param('page')) {
Akron6d49c1f2018-10-11 14:22:21 +0200196 $response->{json}->{meta}->{startIndex} = $v->param("startIndex");
Akron0e1ed242018-10-11 13:22:00 +0200197 };
198
Akron0e1ed242018-10-11 13:22:00 +0200199 # Simple search fixture
Akron32396632018-10-11 17:08:37 +0200200 $c->render(%$response);
201
202 $c->app->log->debug('Rendered result');
203
204 return 1;
Akron0e1ed242018-10-11 13:22:00 +0200205};
206
Akron80a84b22018-10-24 17:44:24 +0200207# Textinfo fixtures
Akron63d963b2019-07-05 15:35:51 +0200208get '/v1.0/corpus/:corpusId/:docId/:textId' => sub {
Akron80a84b22018-10-24 17:44:24 +0200209 my $c = shift;
210
211 my $file = join('_', (
212 'textinfo',
213 $c->stash('corpusId'),
214 $c->stash('docId'),
215 $c->stash('textId')
216 ));
217
218 my $slug = slugify($file);
219
220 # Get response based on query parameter
221 my $response = $c->load_response($slug);
222 return $c->render(%$response);
223};
224
Akron0e1ed242018-10-11 13:22:00 +0200225
Akronb80341d2018-10-15 19:46:23 +0200226# Matchinfo fixtures
Akron63d963b2019-07-05 15:35:51 +0200227get '/v1.0/corpus/:corpusId/:docId/:textId/:matchId/matchInfo' => sub {
Akronb80341d2018-10-15 19:46:23 +0200228 my $c = shift;
229
230 my $file = join('_', (
231 'matchinfo',
232 $c->stash('corpusId'),
233 $c->stash('docId'),
234 $c->stash('textId'),
235 $c->stash('matchId')
236 ));
237
Akronb8d0b402018-10-18 23:51:52 +0200238 my $slug = slugify($file);
239
Akronb80341d2018-10-15 19:46:23 +0200240 # Get response based on query parameter
Akronb8d0b402018-10-18 23:51:52 +0200241 my $response = $c->load_response($slug);
Akronb80341d2018-10-15 19:46:23 +0200242 return $c->render(%$response);
243};
244
Akron0e1ed242018-10-11 13:22:00 +0200245
Akronbe61f4c2018-10-20 00:52:58 +0200246# Statistics endpoint
Akron63d963b2019-07-05 15:35:51 +0200247get '/v1.0/statistics' => sub {
Akronbe61f4c2018-10-20 00:52:58 +0200248 my $c = shift;
249 my $v = $c->validation;
Akron5fa61e92019-07-15 11:56:11 +0200250 $v->optional('cq');
Akronbe61f4c2018-10-20 00:52:58 +0200251
252 my @list = 'corpusinfo';
Akron5fa61e92019-07-15 11:56:11 +0200253 if ($v->param('cq')) {
254 push @list, $v->param('cq');
Akronbe61f4c2018-10-20 00:52:58 +0200255 };
256 my $slug = slugify(join('_', @list));
257
258 # Get response based on query parameter
259 my $response = $c->load_response($slug);
260 return $c->render(%$response);
261};
262
Akron0e1ed242018-10-11 13:22:00 +0200263############
264# Auth API #
265############
266
267# Request API token
Akron63d963b2019-07-05 15:35:51 +0200268get '/v1.0/auth/logout' => sub {
Akron0e1ed242018-10-11 13:22:00 +0200269 my $c = shift;
270
271 if (my $auth = $c->req->headers->header('Authorization')) {
Akroncdfd9d52019-07-23 11:35:00 +0200272
273 if ($auth =~ /^Bearer/) {
274 $c->app->log->debug('Server-Logout: ' . $auth);
275 return $c->render(json => { msg => [[0, 'Fine!']]});
276 }
277
278 elsif (my $jwt = $c->jwt_decode($auth)) {
Akron0e1ed242018-10-11 13:22:00 +0200279 my $user = $jwt->{username} if $jwt->{username};
280
281 $c->app->log->debug('Server-Logout: ' . $user);
282 return $c->render(json => { msg => [[0, 'Fine!']]});
283 };
284 };
285
286 return $c->render(status => 400, json => { error => [[0, 'No!']]});
287};
288
289
290# Request API token
Akron63d963b2019-07-05 15:35:51 +0200291get '/v1.0/auth/apiToken' => sub {
Akron0e1ed242018-10-11 13:22:00 +0200292 my $c = shift;
293
294 # Get auth header
295 my $auth = $c->req->headers->authorization;
296
297 # Authorization missing or not basic
298 if (!$auth || $auth !~ s/\s*Basic\s+//gi) {
299 return $c->render(
300 json => {
301 error => [[2, 'x']]
302 }
303 );
304 };
305
306 # Decode header
307 my ($username, $pwd) = @{b($auth)->b64_decode->split(':')->to_array};
308
309 # the password is 'pass'
310 if ($pwd) {
311
312 # the password is 'pass'
313 if ($pwd eq 'pass') {
314
315 # Render info with token
316 my $jwt = $c->jwt_encode(username => $username);
317
318 # Render in the Kustvakt fashion:
319 return $c->render(
320 format => 'html',
321 text => encode_json({
322 %{$jwt->claims},
323 expires => $jwt->expires,
324 token => $jwt->encode,
325 token_type => 'api_token'
326 })
327 );
Akron3d673062019-01-29 15:54:16 +0100328 }
329
330 elsif ($pwd eq 'ldaperr') {
331 return $c->render(
332 format => 'html',
333 status => 401,
334 json => {
335 "errors" => [[2022,"LDAP Authentication failed due to unknown user or password!"]]
336 }
337 );
Akron0e1ed242018-10-11 13:22:00 +0200338 };
339
340 return $c->render(
341 json => {
342 error => [[2004, undef]]
343 }
344 );
345 };
346
347 return $c->render(
348 json => {
349 error => [[2004, undef]]
350 }
351 );
352};
353
Akron33f5c672019-06-24 19:40:47 +0200354
355# Request API token
Akron63d963b2019-07-05 15:35:51 +0200356post '/v1.0/oauth2/token' => sub {
Akron33f5c672019-06-24 19:40:47 +0200357 my $c = shift;
358
Akron63d963b2019-07-05 15:35:51 +0200359 my $grant_type = $c->param('grant_type') // 'undefined';
360
361 if ($grant_type eq 'password') {
Akron33f5c672019-06-24 19:40:47 +0200362
Akron8bbbecf2019-07-01 18:57:30 +0200363 # Check for wrong client id
364 if ($c->param('client_id') ne '2') {
365 return $c->render(
366 json => {
367 "error_description" => "Unknown client with " . $_->{client_id},
368 "error" => "invalid_client"
369 },
370 status => 401
371 );
372 }
Akron33f5c672019-06-24 19:40:47 +0200373
Akron8bbbecf2019-07-01 18:57:30 +0200374 # Check for wrong client secret
375 elsif ($c->param('client_secret') ne 'k414m4r-s3cr3t') {
376 return $c->render(
377 json => {
378 "error_description" => "Invalid client credentials",
379 "error" => "invalid_client"
380 },
381 status => 401
382 );
383 }
Akron33f5c672019-06-24 19:40:47 +0200384
Akron8bbbecf2019-07-01 18:57:30 +0200385 # Check for wrong user name
386 elsif ($c->param('username') ne 'test') {
387 return $c->render(json => {
388 error => [[2004, undef]]
389 });
390 }
391
392 # Check for ldap error
393 elsif ($c->param('password') eq 'ldaperr') {
394 return $c->render(
395 format => 'html',
396 status => 401,
397 json => {
398 "errors" => [
399 [
400 2022,
401 "LDAP Authentication failed due to unknown user or password!"
402 ]
403 ]
404 }
405 );
406 }
407
408 # Check for wrong password
409 elsif ($c->param('password') ne 'pass') {
410 return $c->render(json => {
411 format => 'html',
412 status => 401,
Akron33f5c672019-06-24 19:40:47 +0200413 "errors" => [[2022,"LDAP Authentication failed due to unknown user or password!"]]
Akron8bbbecf2019-07-01 18:57:30 +0200414 });
415 }
416
417 # Return fine access
418 return $c->render(
419 json => {
Akroncdfd9d52019-07-23 11:35:00 +0200420 "access_token" => $c->get_token('access_token'),
421 "refresh_token" => $c->get_token('refresh_token'),
Akron8bbbecf2019-07-01 18:57:30 +0200422 "scope" => "all",
423 "token_type" => "Bearer",
424 "expires_in" => 86400
425 });
426 }
427
428 # Refresh token
Akron63d963b2019-07-05 15:35:51 +0200429 elsif ($grant_type eq 'refresh_token') {
Akroncdfd9d52019-07-23 11:35:00 +0200430
431 if ($c->param('refresh_token') eq 'inv4lid') {
432 return $c->render(
433 status => 400,
434 json => {
435 "error_description" => "Refresh token is expired",
436 "error" => "invalid_grant"
437 }
438 );
439 };
440
441 $c->app->log->debug("Refresh the token in the mock server!");
442
Akron8bbbecf2019-07-01 18:57:30 +0200443 return $c->render(
444 status => 200,
445 json => {
Akroncdfd9d52019-07-23 11:35:00 +0200446 "access_token" => $c->get_token("access_token_2"),
447 "refresh_token" => $c->get_token("refresh_token_2"),
Akron8bbbecf2019-07-01 18:57:30 +0200448 "token_type" => "Bearer",
449 "expires_in" => 86400
Akron33f5c672019-06-24 19:40:47 +0200450 }
451 );
452 }
453
Akron8bbbecf2019-07-01 18:57:30 +0200454 # Unknown token grant
455 else {
456 return $c->render(
Akron63d963b2019-07-05 15:35:51 +0200457 status => 400,
Akron8bbbecf2019-07-01 18:57:30 +0200458 json => {
459 "errors" => [
460 [
Akron63d963b2019-07-05 15:35:51 +0200461 0, "Grant Type unknown", $grant_type
Akron8bbbecf2019-07-01 18:57:30 +0200462 ]
463 ]
464 }
465 )
Akron33f5c672019-06-24 19:40:47 +0200466 }
Akron33f5c672019-06-24 19:40:47 +0200467};
468
Akron4cefe1f2019-09-04 10:11:28 +0200469# Revoke API token
470post '/v1.0/oauth2/revoke' => sub {
471 my $c = shift;
472
473 my $refresh_token = $c->param('token');
474
475 if ($c->param('client_secret') ne 'k414m4r-s3cr3t') {
476 return $c->render(
477 json => {
478 "error_description" => "Invalid client credentials",
479 "error" => "invalid_client"
480 },
481 status => 401
482 );
483 };
484
485 return $c->render(
486 text => ''
487 )
488};
Akron33f5c672019-06-24 19:40:47 +0200489
490
Akron0e1ed242018-10-11 13:22:00 +0200491app->start;
492
493
494__END__
495
496
497 # Temporary:
498 my $collection_query = {
499 '@type' => "koral:docGroup",
500 "operation" => "operation:or",
501 "operands" => [
502 {
503 '@type' => "koral:docGroup",
504 "operation" => "operation:and",
505 "operands" => [
506 {
507 '@type' => "koral:doc",
508 "key" => "title",
509 "match" => "match:eq",
510 "value" => "Der Birnbaum",
511 "type" => "type:string"
512 },
513 {
514 '@type' => "koral:doc",
515 "key" => "pubPlace",
516 "match" => "match:eq",
517 "value" => "Mannheim",
518 "type" => "type:string"
519 },
520 {
521 '@type' => "koral:docGroup",
522 "operation" => "operation:or",
523 "operands" => [
524 {
525 '@type' => "koral:doc",
526 "key" => "subTitle",
527 "match" => "match:eq",
528 "value" => "Aufzucht oder Pflege",
529 "type" => "type:string"
530 },
531 {
532 '@type' => "koral:doc",
533 "key" => "subTitle",
534 "match" => "match:eq",
535 "value" => "Gedichte",
536 "type" => "type:string"
537 }
538 ]
539 }
540 ]
541 },
542 {
543 '@type' => "koral:doc",
544 "key" => "pubDate",
545 "match" => "match:geq",
546 "value" => "2015-03-05",
547 "type" => "type:date"
548 }
549 ]
550 };