blob: 2e14035c12676647d7fa41178524f7017c7d8146 [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
Akron0e1ed242018-10-11 13:22:00 +0200103# Search fixtures
Akron63d963b2019-07-05 15:35:51 +0200104get '/v1.0/search' => sub {
Akron0e1ed242018-10-11 13:22:00 +0200105 my $c = shift;
106 my $v = $c->validation;
107 $v->optional('q');
108 $v->optional('page');
109 $v->optional('ql');
Akroncd42a142019-07-12 18:55:37 +0200110 $v->optional('cq');
Akron0e1ed242018-10-11 13:22:00 +0200111 $v->optional('count');
112 $v->optional('context');
Akron8ea84292018-10-24 13:41:52 +0200113 $v->optional('offset');
114 $v->optional('cutoff')->in(qw/true false/);
Akron0e1ed242018-10-11 13:22:00 +0200115
Akron32396632018-10-11 17:08:37 +0200116 $c->app->log->debug('Receive request');
117
Akron0e1ed242018-10-11 13:22:00 +0200118 # Response q=x&ql=cosmas3
119 if ($v->param('ql') && $v->param('ql') eq 'cosmas3') {
120 return $c->render(
121 status => 400,
122 json => {
123 "\@context" => "http://korap.ids-mannheim.de/ns/koral/0.3/context.jsonld",
124 "errors" => [[307,"cosmas3 is not a supported query language!"]]
125 });
126 };
127
Akron6d49c1f2018-10-11 14:22:21 +0200128 if (!$v->param('q')) {
Akron8ea84292018-10-24 13:41:52 +0200129 return $c->render(%{$c->load_response('query_no_query')});
Akron0e1ed242018-10-11 13:22:00 +0200130 };
131
Akron8ea84292018-10-24 13:41:52 +0200132 my @slug_base = ($v->param('q'));
133 push @slug_base, 'o' . $v->param('offset') if defined $v->param('offset');
134 push @slug_base, 'c' . $v->param('count') if defined $v->param('count');
135 push @slug_base, 'co' . $v->param('cutoff') if defined $v->param('cutoff');
Akroncd42a142019-07-12 18:55:37 +0200136 push @slug_base, 'cq' if defined $v->param('cq');
Akron8ea84292018-10-24 13:41:52 +0200137
Akron6d49c1f2018-10-11 14:22:21 +0200138 # Get response based on query parameter
Akron8ea84292018-10-24 13:41:52 +0200139 my $response = $c->load_response('query_' . slugify(join('_', @slug_base)));
Akron0e1ed242018-10-11 13:22:00 +0200140
141 # Check authentification
142 if (my $auth = $c->req->headers->header('Authorization')) {
Akron33f5c672019-06-24 19:40:47 +0200143
Akroncdfd9d52019-07-23 11:35:00 +0200144 $c->app->log->debug("There is an authorization header $auth");
Akron33f5c672019-06-24 19:40:47 +0200145 my $jwt;
146 if ($auth =~ /^Bearer/) {
147 # Username unknown in OAuth2
148 $response->{json}->{meta}->{authorized} = 'yes';
149 }
Akroncdfd9d52019-07-23 11:35:00 +0200150 elsif ($auth =~ /^api_token/ && ($jwt = $c->jwt_decode($auth))) {
Akron6d49c1f2018-10-11 14:22:21 +0200151 $response->{json}->{meta}->{authorized} = $jwt->{username} if $jwt->{username};
Akron0e1ed242018-10-11 13:22:00 +0200152 };
Akroncdfd9d52019-07-23 11:35:00 +0200153
154 # Code is expired
155 if ($c->expired($auth)) {
156
157 $c->app->log->debug("The access token has expired");
158
159 return $c->render(
160 status => 401,
161 json => {
162 errors => [[2003, 'Access token is expired']]
163 }
164 );
165 }
166
167 # Auth token is invalid
168 if ($auth =~ /^Bearer inv4lid/) {
169 $c->app->log->debug("The access token is invalid");
170
171 return $c->render(
172 status => 401,
173 json => {
174 errors => [[2011, 'Access token is invalid']]
175 }
176 );
177 }
Akron0e1ed242018-10-11 13:22:00 +0200178 };
179
Akron6d49c1f2018-10-11 14:22:21 +0200180 # Set page parameter
Akron0e1ed242018-10-11 13:22:00 +0200181 if ($v->param('page')) {
Akron6d49c1f2018-10-11 14:22:21 +0200182 $response->{json}->{meta}->{startIndex} = $v->param("startIndex");
Akron0e1ed242018-10-11 13:22:00 +0200183 };
184
Akron0e1ed242018-10-11 13:22:00 +0200185 # Simple search fixture
Akron32396632018-10-11 17:08:37 +0200186 $c->render(%$response);
187
188 $c->app->log->debug('Rendered result');
189
190 return 1;
Akron0e1ed242018-10-11 13:22:00 +0200191};
192
Akron80a84b22018-10-24 17:44:24 +0200193# Textinfo fixtures
Akron63d963b2019-07-05 15:35:51 +0200194get '/v1.0/corpus/:corpusId/:docId/:textId' => sub {
Akron80a84b22018-10-24 17:44:24 +0200195 my $c = shift;
196
197 my $file = join('_', (
198 'textinfo',
199 $c->stash('corpusId'),
200 $c->stash('docId'),
201 $c->stash('textId')
202 ));
203
204 my $slug = slugify($file);
205
206 # Get response based on query parameter
207 my $response = $c->load_response($slug);
208 return $c->render(%$response);
209};
210
Akron0e1ed242018-10-11 13:22:00 +0200211
Akronb80341d2018-10-15 19:46:23 +0200212# Matchinfo fixtures
Akron63d963b2019-07-05 15:35:51 +0200213get '/v1.0/corpus/:corpusId/:docId/:textId/:matchId/matchInfo' => sub {
Akronb80341d2018-10-15 19:46:23 +0200214 my $c = shift;
215
216 my $file = join('_', (
217 'matchinfo',
218 $c->stash('corpusId'),
219 $c->stash('docId'),
220 $c->stash('textId'),
221 $c->stash('matchId')
222 ));
223
Akronb8d0b402018-10-18 23:51:52 +0200224 my $slug = slugify($file);
225
Akronb80341d2018-10-15 19:46:23 +0200226 # Get response based on query parameter
Akronb8d0b402018-10-18 23:51:52 +0200227 my $response = $c->load_response($slug);
Akronb80341d2018-10-15 19:46:23 +0200228 return $c->render(%$response);
229};
230
Akron0e1ed242018-10-11 13:22:00 +0200231
Akronbe61f4c2018-10-20 00:52:58 +0200232# Statistics endpoint
Akron63d963b2019-07-05 15:35:51 +0200233get '/v1.0/statistics' => sub {
Akronbe61f4c2018-10-20 00:52:58 +0200234 my $c = shift;
235 my $v = $c->validation;
Akron5fa61e92019-07-15 11:56:11 +0200236 $v->optional('cq');
Akronbe61f4c2018-10-20 00:52:58 +0200237
238 my @list = 'corpusinfo';
Akron5fa61e92019-07-15 11:56:11 +0200239 if ($v->param('cq')) {
240 push @list, $v->param('cq');
Akronbe61f4c2018-10-20 00:52:58 +0200241 };
242 my $slug = slugify(join('_', @list));
243
244 # Get response based on query parameter
245 my $response = $c->load_response($slug);
246 return $c->render(%$response);
247};
248
Akron0e1ed242018-10-11 13:22:00 +0200249############
250# Auth API #
251############
252
253# Request API token
Akron63d963b2019-07-05 15:35:51 +0200254get '/v1.0/auth/logout' => sub {
Akron0e1ed242018-10-11 13:22:00 +0200255 my $c = shift;
256
257 if (my $auth = $c->req->headers->header('Authorization')) {
Akroncdfd9d52019-07-23 11:35:00 +0200258
259 if ($auth =~ /^Bearer/) {
260 $c->app->log->debug('Server-Logout: ' . $auth);
261 return $c->render(json => { msg => [[0, 'Fine!']]});
262 }
263
264 elsif (my $jwt = $c->jwt_decode($auth)) {
Akron0e1ed242018-10-11 13:22:00 +0200265 my $user = $jwt->{username} if $jwt->{username};
266
267 $c->app->log->debug('Server-Logout: ' . $user);
268 return $c->render(json => { msg => [[0, 'Fine!']]});
269 };
270 };
271
272 return $c->render(status => 400, json => { error => [[0, 'No!']]});
273};
274
275
276# Request API token
Akron63d963b2019-07-05 15:35:51 +0200277get '/v1.0/auth/apiToken' => sub {
Akron0e1ed242018-10-11 13:22:00 +0200278 my $c = shift;
279
280 # Get auth header
281 my $auth = $c->req->headers->authorization;
282
283 # Authorization missing or not basic
284 if (!$auth || $auth !~ s/\s*Basic\s+//gi) {
285 return $c->render(
286 json => {
287 error => [[2, 'x']]
288 }
289 );
290 };
291
292 # Decode header
293 my ($username, $pwd) = @{b($auth)->b64_decode->split(':')->to_array};
294
295 # the password is 'pass'
296 if ($pwd) {
297
298 # the password is 'pass'
299 if ($pwd eq 'pass') {
300
301 # Render info with token
302 my $jwt = $c->jwt_encode(username => $username);
303
304 # Render in the Kustvakt fashion:
305 return $c->render(
306 format => 'html',
307 text => encode_json({
308 %{$jwt->claims},
309 expires => $jwt->expires,
310 token => $jwt->encode,
311 token_type => 'api_token'
312 })
313 );
Akron3d673062019-01-29 15:54:16 +0100314 }
315
316 elsif ($pwd eq 'ldaperr') {
317 return $c->render(
318 format => 'html',
319 status => 401,
320 json => {
321 "errors" => [[2022,"LDAP Authentication failed due to unknown user or password!"]]
322 }
323 );
Akron0e1ed242018-10-11 13:22:00 +0200324 };
325
326 return $c->render(
327 json => {
328 error => [[2004, undef]]
329 }
330 );
331 };
332
333 return $c->render(
334 json => {
335 error => [[2004, undef]]
336 }
337 );
338};
339
Akron33f5c672019-06-24 19:40:47 +0200340
341# Request API token
Akron63d963b2019-07-05 15:35:51 +0200342post '/v1.0/oauth2/token' => sub {
Akron33f5c672019-06-24 19:40:47 +0200343 my $c = shift;
344
Akron63d963b2019-07-05 15:35:51 +0200345 my $grant_type = $c->param('grant_type') // 'undefined';
346
347 if ($grant_type eq 'password') {
Akron33f5c672019-06-24 19:40:47 +0200348
Akron8bbbecf2019-07-01 18:57:30 +0200349 # Check for wrong client id
350 if ($c->param('client_id') ne '2') {
351 return $c->render(
352 json => {
353 "error_description" => "Unknown client with " . $_->{client_id},
354 "error" => "invalid_client"
355 },
356 status => 401
357 );
358 }
Akron33f5c672019-06-24 19:40:47 +0200359
Akron8bbbecf2019-07-01 18:57:30 +0200360 # Check for wrong client secret
361 elsif ($c->param('client_secret') ne 'k414m4r-s3cr3t') {
362 return $c->render(
363 json => {
364 "error_description" => "Invalid client credentials",
365 "error" => "invalid_client"
366 },
367 status => 401
368 );
369 }
Akron33f5c672019-06-24 19:40:47 +0200370
Akron8bbbecf2019-07-01 18:57:30 +0200371 # Check for wrong user name
372 elsif ($c->param('username') ne 'test') {
373 return $c->render(json => {
374 error => [[2004, undef]]
375 });
376 }
377
378 # Check for ldap error
379 elsif ($c->param('password') eq 'ldaperr') {
380 return $c->render(
381 format => 'html',
382 status => 401,
383 json => {
384 "errors" => [
385 [
386 2022,
387 "LDAP Authentication failed due to unknown user or password!"
388 ]
389 ]
390 }
391 );
392 }
393
394 # Check for wrong password
395 elsif ($c->param('password') ne 'pass') {
396 return $c->render(json => {
397 format => 'html',
398 status => 401,
Akron33f5c672019-06-24 19:40:47 +0200399 "errors" => [[2022,"LDAP Authentication failed due to unknown user or password!"]]
Akron8bbbecf2019-07-01 18:57:30 +0200400 });
401 }
402
403 # Return fine access
404 return $c->render(
405 json => {
Akroncdfd9d52019-07-23 11:35:00 +0200406 "access_token" => $c->get_token('access_token'),
407 "refresh_token" => $c->get_token('refresh_token'),
Akron8bbbecf2019-07-01 18:57:30 +0200408 "scope" => "all",
409 "token_type" => "Bearer",
410 "expires_in" => 86400
411 });
412 }
413
414 # Refresh token
Akron63d963b2019-07-05 15:35:51 +0200415 elsif ($grant_type eq 'refresh_token') {
Akroncdfd9d52019-07-23 11:35:00 +0200416
417 if ($c->param('refresh_token') eq 'inv4lid') {
418 return $c->render(
419 status => 400,
420 json => {
421 "error_description" => "Refresh token is expired",
422 "error" => "invalid_grant"
423 }
424 );
425 };
426
427 $c->app->log->debug("Refresh the token in the mock server!");
428
Akron8bbbecf2019-07-01 18:57:30 +0200429 return $c->render(
430 status => 200,
431 json => {
Akroncdfd9d52019-07-23 11:35:00 +0200432 "access_token" => $c->get_token("access_token_2"),
433 "refresh_token" => $c->get_token("refresh_token_2"),
Akron8bbbecf2019-07-01 18:57:30 +0200434 "token_type" => "Bearer",
435 "expires_in" => 86400
Akron33f5c672019-06-24 19:40:47 +0200436 }
437 );
438 }
439
Akron8bbbecf2019-07-01 18:57:30 +0200440 # Unknown token grant
441 else {
442 return $c->render(
Akron63d963b2019-07-05 15:35:51 +0200443 status => 400,
Akron8bbbecf2019-07-01 18:57:30 +0200444 json => {
445 "errors" => [
446 [
Akron63d963b2019-07-05 15:35:51 +0200447 0, "Grant Type unknown", $grant_type
Akron8bbbecf2019-07-01 18:57:30 +0200448 ]
449 ]
450 }
451 )
Akron33f5c672019-06-24 19:40:47 +0200452 }
Akron33f5c672019-06-24 19:40:47 +0200453};
454
Akron4cefe1f2019-09-04 10:11:28 +0200455# Revoke API token
456post '/v1.0/oauth2/revoke' => sub {
457 my $c = shift;
458
459 my $refresh_token = $c->param('token');
460
461 if ($c->param('client_secret') ne 'k414m4r-s3cr3t') {
462 return $c->render(
463 json => {
464 "error_description" => "Invalid client credentials",
465 "error" => "invalid_client"
466 },
467 status => 401
468 );
469 };
470
471 return $c->render(
472 text => ''
473 )
474};
Akron33f5c672019-06-24 19:40:47 +0200475
476
Akron0e1ed242018-10-11 13:22:00 +0200477app->start;
478
479
480__END__
481
482
483 # Temporary:
484 my $collection_query = {
485 '@type' => "koral:docGroup",
486 "operation" => "operation:or",
487 "operands" => [
488 {
489 '@type' => "koral:docGroup",
490 "operation" => "operation:and",
491 "operands" => [
492 {
493 '@type' => "koral:doc",
494 "key" => "title",
495 "match" => "match:eq",
496 "value" => "Der Birnbaum",
497 "type" => "type:string"
498 },
499 {
500 '@type' => "koral:doc",
501 "key" => "pubPlace",
502 "match" => "match:eq",
503 "value" => "Mannheim",
504 "type" => "type:string"
505 },
506 {
507 '@type' => "koral:docGroup",
508 "operation" => "operation:or",
509 "operands" => [
510 {
511 '@type' => "koral:doc",
512 "key" => "subTitle",
513 "match" => "match:eq",
514 "value" => "Aufzucht oder Pflege",
515 "type" => "type:string"
516 },
517 {
518 '@type' => "koral:doc",
519 "key" => "subTitle",
520 "match" => "match:eq",
521 "value" => "Gedichte",
522 "type" => "type:string"
523 }
524 ]
525 }
526 ]
527 },
528 {
529 '@type' => "koral:doc",
530 "key" => "pubDate",
531 "match" => "match:geq",
532 "value" => "2015-03-05",
533 "type" => "type:date"
534 }
535 ]
536 };