File Coverage

blib/lib/Mojolicious/Plugin/TagHelpers/MailToChiffre.pm
Criterion Covered Total %
statement 173 184 94.0
branch 45 56 80.3
condition 8 14 57.1
subroutine 16 16 100.0
pod 1 3 33.3
total 243 273 89.0


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::TagHelpers::MailToChiffre;
2 2     2   2125 use Mojo::Base 'Mojolicious::Plugin';
  2         5  
  2         18  
3 2     2   485 use Mojo::ByteStream 'b';
  2         18  
  2         107  
4 2     2   12 use Mojo::Collection 'c';
  2         5  
  2         91  
5 2     2   14 use Mojo::URL;
  2         4  
  2         19  
6              
7             our $VERSION = '0.12';
8              
9             # Cache for generated CSS and JavaScript
10             has [qw/js css pattern_rotate/];
11              
12             # Register Plugin
13             sub register {
14 5     5 1 26401 my ($plugin, $app, $plugin_param) = @_;
15              
16             # Load random string plugin with specific profile
17 5         52 $app->plugin('Util::RandomString' => {
18             mail_to_chiffre => {
19             alphabet => 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
20             entropy => 128
21             }
22             });
23              
24 5         8972 delete $plugin->{js};
25 5         15 delete $plugin->{css};
26              
27             # Load parameters from Config file
28 5 50       29 if (my $config_param = $app->config('TagHelpers-MailToChiffre')) {
29 0         0 $plugin_param = { %$config_param, %$plugin_param };
30             };
31              
32             # Generate method name in case it is not given
33 5   66     111 my $method_name = $plugin_param->{method_name} // $app->random_string('mail_to_chiffre');
34              
35             # Set pattern shift in case it is not given
36 5         1682 my $pattern_rotate = 2;
37 5 50 33     57 if ($plugin_param->{pattern_rotate} && $plugin_param->{pattern_rotate} =~ /^\d+$/) {
38 5         16 $pattern_rotate = $plugin_param->{pattern_rotate};
39             };
40 5         21 $plugin->pattern_rotate($pattern_rotate);
41              
42 5   100     66 $plugin->{no_inline} = $plugin_param->{no_inline} // undef;
43              
44             # Add pseudo condition for manipulating the stash for the fallback
45 5         29 my $routes = $app->routes;
46              
47             # Add fallback shortcut
48             $routes->add_shortcut(
49             mail_to_chiffre => sub {
50 2     2   2721 my $r = shift;
51              
52 2         6 state $name = 'mailToChiffre';
53              
54             # In case method name is given, set asset paths
55 2 100       9 if ($plugin_param->{method_name}) {
56              
57             # Styles
58             $r->get('/style.css')->to(
59             cb => sub {
60 0         0 my $c = shift;
61 0         0 $c->render(
62             text => $c->mail_to_chiffre_css,
63             format => 'css'
64             );
65             }
66 1         6 )->name($name . 'CSS');
67              
68             # Styles
69             $r->get('/script.js')->to(
70             cb => sub {
71 0         0 my $c = shift;
72 0         0 $c->render(
73             text => $c->mail_to_chiffre_js,
74             format => 'js'
75             );
76             }
77 1         333 )->name($name . 'JS');
78             };
79              
80             # Fallback path
81             $r->under('/:xor/:host')->to(
82             cb => sub {
83 12         143006 $plugin->_chiffre_to_mail(shift)
84             }
85 2         328 )->get('/')->name($name)->to(@_);
86             }
87 5         68 );
88              
89              
90             # Add obfuscation tag helper
91             $app->helper(
92             mail_to_chiffre => sub {
93 17     17   65713 my $c = shift;
94              
95 17 100       67 my $address = shift or return b('');
96              
97             # Create one time pad
98 16         64 my $xor = substr($c->random_string('mail_to_chiffre'), 0, length($address));
99              
100             # Get embedded code
101 16         1787 my $text;
102 16 50 66     76 if (ref($_[-1]) && ref($_[-1]) eq 'CODE') {
103 0         0 $text = pop;
104             };
105              
106 16         85 my %param = @_;
107              
108             # Split the address and do some encodings
109 16         57 my $obf_address = b($address)->xml_escape->split('@');
110 16         1083 my $account = $obf_address->first;
111              
112 16         142 my $host = join '@', @{$obf_address}[1 .. $obf_address->size - 1];
  16         122  
113              
114             # Reget the pattern rotate (maybe)
115 16         122 my $pattern_rotate = $plugin->pattern_rotate;
116              
117             # Obfuscate address parts
118 16         107 $host = $plugin->to_sequence(
119             $host,
120             $xor,
121             $pattern_rotate
122             );
123              
124 16         57 $account = $plugin->to_sequence(
125             $account,
126             $xor,
127             $pattern_rotate
128             );
129              
130             # Create Mojo::URL for path
131 16         32 my ($url, $no_fallback);
132 16 100       72 if ($routes->lookup('mailToChiffre')) {
133 14         314 $url = $c->url_for('mailToChiffre', xor => $xor, host => $host);
134             }
135             else {
136 2         77 $url = $c->url_for("/$xor/$host");
137 2         885 $no_fallback = 1;
138             };
139              
140             # Encrypt certain mail parameters
141 16         7343 foreach (qw/to cc bcc/) {
142              
143             # No parameter
144 48 100       133 next unless exists $param{$_};
145              
146             # Parameter invalid
147 10 100       29 unless ($param{$_}) {
148 1         3 delete $param{$_};
149 1         4 next;
150             };
151              
152             # Array for this parameter
153 9 100       35 if (ref $param{$_}) {
154 3         13 my @temp;
155 3         17 foreach (@{$param{$_}}) {
  3         16  
156 4 50       25 push(@temp, $plugin->to_sequence($_, $xor, $pattern_rotate)) if $_;
157             };
158              
159             # Check if there are converted parameters
160 3 100       18 if (@temp) {
161 2         15 $param{$_} = \@temp;
162             }
163             # Remove parameter from list
164             else {
165 1         6 delete $param{$_};
166             };
167             }
168              
169             # Single value
170             else {
171             $param{$_} = $plugin->to_sequence(
172 6         29 $param{$_},
173             $xor,
174             $pattern_rotate
175             );
176             };
177             };
178              
179             # Return path
180 16         127 $url->query({sid => $account, %param});
181              
182             # Create anchor link
183 16         1962 my $str = qq!
184              
185             # No fallback is established
186 16 100       41 if ($no_fallback) {
187              
188             # Do not establish a URL at all
189 2 100       15 if ($plugin->{no_inline}) {
190 1         5 $str .= qq!href="#" data-href="$url" !;
191             }
192              
193             # Use javascript fallback
194             else {
195 1         6 $str .= qq!href="javascript:$method_name(false,'$url')" !;
196             };
197             }
198              
199             else {
200 14         52 $str .= qq!href="$url" !;
201             };
202              
203 16 100       7223 if ($plugin->{no_inline}) {
204 1         41 $str .= 'class="' . $method_name;
205             } else {
206 15         34 $str .= 'onclick="';
207 15 100       45 $str .= 'return true;' if $no_fallback;
208 15         44 $str .= 'return ' . $method_name . '(this,false)';
209             };
210              
211             # Obfuscate display string using css
212 16 50       53 unless ($text) {
213 16         74 my ($pre, @post) = split('@', reverse($address));
214 16         61 $str .= '">' .
215             '' . b($pre)->xml_escape . '' .
216             '' . b($xor)->split('')->reverse->join . '' .
217             c(@post)->join->xml_escape;
218             }
219             else {
220 0         0 $str .= ';' . int(rand(50)) . '">' . $text->();
221             };
222              
223 16         4786 $str .= '';
224              
225 16         47 return b($str);
226             }
227 5         379 );
228              
229             # Create css code helper
230             $app->helper(
231             mail_to_chiffre_css => sub {
232 6 100   6   3630 return $plugin->css if $plugin->css;
233 4         25 my $css;
234 4 100       16 if ($plugin->{no_inline}) {
235 1         3 $css = qq!a.$method_name!;
236             } else {
237 3         13 $css = qq!a[onclick\$='return $method_name(this,false)']!;
238             };
239 4         21 $css = $css . '{direction:rtl;unicode-bidi:bidi-override;text-align:left}'.
240             $css . '>span:nth-child(1n+2){display:none}' .
241             $css . '>span:nth-child(1):after{content:\'@\'}';
242 4         37 $plugin->css(b($css));
243 4         89 return $plugin->css;
244             }
245 5         395 );
246              
247              
248             # Create javascript code helper
249             $app->helper(
250             mail_to_chiffre_js => sub {
251 4     4   2730 my $c = shift;
252              
253 4 50       28 return $plugin->js if $plugin->js;
254              
255             # Replacement variables
256 4         42 my $v = c(qw/o s u c p n t r g f a x e d q b l m k/)->shuffle;
257              
258             # Template variables
259 4         217 my ($i, %v) = (0);
260 4         14 foreach (qw/obj seq url char pos num str regex string_obj
261             from_char_code param_array temp to_seq
262             path_array query padded str_len pow bool/) {
263 76         193 $v{$_} = $v->[$i++];
264             };
265              
266             # Obfuscate pattern rotate
267 4         16 my $factor_pattern_rotate = _factorize($plugin->pattern_rotate, $v{pow});
268              
269             # Create javascript code
270 4         150 my $js = qq!function ${method_name}($v{obj},$v{bool}){
271             if($v{bool}){
272             $v{obj}=document.createElement('a');$v{obj}.href=$v{bool}
273             }
274             var $v{query}=$v{obj}.search,$v{regex}=RegExp,$v{from_char_code}=String.fromCharCode,$v{url}='il',$v{param_array}=[],$v{temp},$v{pow}=Math.pow;
275             $v{path_array}=$v{obj}.pathname.match(/([^\\/]+)\\/([^\\/]+)\$/);
276             $v{to_seq}=function($v{seq}){
277             var $v{pos}=0,$v{num},$v{str}='',$v{char};
278             while($v{pos}<$v{seq}.length){
279             $v{char}=$v{seq}.charAt($v{pos}++);
280             if($v{char}.match(/[A-Za-z]/)){
281             $v{str}+=$v{from_char_code}(($v{char}<='Z'?90:122)>=($v{char}=$v{char}.charCodeAt(0)+13)?$v{char}:$v{char}-26)
282             }
283             else if($v{char}=='-'){
284             $v{num}='';
285             $v{char}=$v{seq}.charAt($v{pos}++);
286             while($v{char}.match(/\\d/)){
287             $v{num}+=$v{char};
288             $v{char}=$v{seq}.charAt($v{pos}++)
289             }
290             $v{pos}--;
291             $v{str}+=$v{from_char_code}(parseInt($v{num}))
292             }
293             else return
294             }
295             $v{str_len}=$v{str}.length;
296             $v{padded}=Math.abs(${factor_pattern_rotate}%$v{str_len}-$v{str_len});
297             $v{str}=$v{str}.substr($v{padded})+$v{str}.substr(0,$v{padded});
298             $v{temp}='';
299             for(i=0;i<$v{str_len};i++){
300             $v{temp}+=$v{from_char_code}($v{str}.charCodeAt(i)^$v{path_array}\[1\].charCodeAt($v{path_array}\[1\].length%(i+1)))
301             }
302             return $v{temp}
303             };
304             while($v{query}){
305             $v{query}=$v{query}.replace(/^[\\?\\&]([^\\&]+)/,'');
306             $v{temp}=$v{regex}.\$1;
307             if($v{temp}.match(/^(sid|b?cc|to)=(.+)\$/)){
308             if($v{regex}.\$1=='sid')
309             $v{param_array}.push('to='+$v{to_seq}($v{regex}.\$2)+'\@'+$v{to_seq}($v{path_array}\[2\]));
310             else $v{param_array}.push($v{regex}.\$1+'='+$v{to_seq}($v{regex}.\$2));
311             }else $v{param_array}.push($v{temp}.replace(/\\+/g,' '))
312             }
313             location.href='ma'+$v{url}+'to:?'+$v{param_array}.join('&');
314             return false
315             }!;
316             # csp compliant variant
317 4 100       18 if ($plugin->{no_inline}) {
318 1         5 $js .= qq!
319             ;document.addEventListener("DOMContentLoaded",
320             function(){
321             document.querySelectorAll(".${method_name}").forEach(
322             i=>i.addEventListener(
323             "click",function(e){
324             e.preventDefault();
325             ${method_name}(false,this.href=='#'?this.href:this.getAttribute('data-href'))
326             }
327             )
328             )
329             }
330             )!;
331             };
332              
333 4         196 $js =~ s/\s*\n\s*//g;
334 4         26 $plugin->js(b($js));
335 4         71 return $plugin->js;
336             }
337 5         431 );
338             };
339              
340              
341             sub _chiffre_to_mail {
342 12     12   27 my ($plugin, $c) = @_;
343 12         36 my $xor = $c->stash('xor');
344 12         157 my $p = $c->req->url->query;
345              
346             # Set header for searc engines
347 12         287 $c->res->headers->header('X-Robots-Tag' => 'noindex,nofollow');
348              
349             # Deobfuscate host
350 12         557 my $host = $plugin->to_string(
351             $c->stash('host'),
352             $xor,
353             $plugin->pattern_rotate
354             );
355              
356             # Deobfuscate account
357 12         44 my $account = $plugin->to_string(
358             scalar $p->param('sid'),
359             $xor,
360             $plugin->pattern_rotate
361             );
362 12         51 $p->remove('sid');
363              
364             # Something went wrong
365 12 50 33     258 unless ($host && $account) {
366 0         0 $c->app->log->warn('Path doesn\'t contain a valid email address');
367 0         0 return;
368             };
369              
370             # Create url
371 12         45 my $url = Mojo::URL->new;
372 12         155 $url->scheme('mailto');
373 12         107 $url->path($account . '@' . $host);
374              
375             # Deobfuscate further address parameters
376 12         2542 foreach my $type (qw/to cc bcc/) {
377 36 100       468 if (my @val = @{$p->every_param($type)}) {
  36         120  
378              
379             # Delete obfuscated parameters
380 8         188 $p->remove($type);
381              
382             # Append new deobfuscated parameters
383             $p->append($type => [map {
384 8         128 $plugin->to_string(
  10         32  
385             $_,
386             $xor,
387             $plugin->pattern_rotate
388             )
389             } @val]);
390             };
391             };
392              
393 12         218 $url->query->append($p);
394              
395             # Store the deobfuscated mail in the stash
396 12         507 $c->stash(mail_to_chiffre => $url);
397              
398 12         255 return 1;
399             };
400              
401              
402             # Simple string based xor function with looping key
403             sub _xor {
404 76     76   141 my $str = '';
405 76         217 for (my $i = 0; $i < length($_[0]); $i++) {
406 873         2725 $str .= substr($_[0], $i, 1) ^ substr($_[1], length($_[1]) % ($i + 1), 1);
407             };
408 76         361 return $str;
409             };
410              
411              
412             # Rotate with pattern
413             sub _rotate {
414 42     42   82 my $p = $_[1] % length($_[0]);
415 42         163 substr($_[0], $p) . substr($_[0], 0, $p)
416             };
417              
418              
419             # Unrotate with pattern_rotate
420             sub _unrotate {
421 34     34   89 my $p = abs($_[1] % length($_[0]) - length($_[0]));
422 34         119 substr($_[0], $p) . substr($_[0], 0, $p);
423             };
424              
425              
426             # Obfuscate the pattern shift a little bit
427             # by simple prime factorization
428             sub _factorize {
429 4     4   27 my $x = shift;
430 4         14 my %factors;
431 4         11 foreach (qw/2 3 5 7/) {
432 16         67 while (!($x % $_)) {
433 10         22 $factors{$_}++;
434 10         30 $x = $x / $_;
435             };
436             };
437 4         10 my @factors;
438 4         31 foreach (keys %factors) {
439 4 50       16 if ($factors{$_} > 1) {
440 4         22 push(@factors, $_[0] . '(' . $_ . ',' . $factors{$_} . ')');
441             }
442             else {
443 0         0 push(@factors, $_);
444             };
445             };
446 4 50       14 push(@factors, $x) unless $x == 1;
447 4         19 return join('*', @factors);
448             };
449              
450              
451             # Serialize to string
452             sub to_string {
453 34     34 0 2242 shift;
454 34 50       96 my $seq = shift or return;
455 34         77 my ($xor, $p) = @_;
456              
457 34         62 my ($str, $c, $num);
458 34         65 my $pos = 0;
459              
460 34         96 my $length = length $seq;
461              
462             # parse sequence
463 34         97 while ($pos < $length) {
464 392         755 $c = substr($seq, $pos++, 1);
465              
466             # Parse alphabetical character (ROT13)
467 392 100       895 if ($c =~ tr/n-za-mN-ZA-M/a-zA-Z/) {
    50          
468 18         38 $str .= $c;
469             }
470              
471             # Parse number
472             elsif ($c eq '-') {
473 374         583 $num = '';
474 374         627 $c = substr($seq, $pos++, 1);
475              
476             # Collect number segments
477 374         1058 while ($c =~ /[0-9]/) {
478 701         1206 $num .= $c;
479 701         1969 $c = substr($seq, $pos++, 1);
480             };
481              
482 374         591 $pos--;
483 374         1041 $str .= chr($num);
484             }
485              
486             # Error
487             else {
488 0         0 return;
489             };
490             };
491 34         83 return _xor(_unrotate($str, $p), $xor);
492             };
493              
494              
495             # Serialize to sequence
496             sub to_sequence {
497 42     42 0 72 shift;
498 42         107 my ($s, $k, $p) = @_;
499              
500             # _xor is not allowed to be null
501 42         103 my $src = _rotate(_xor($s, $k), $p);
502 42         87 my $str;
503              
504             # Parse string
505 42         183 foreach my $c (split('', $src)) {
506              
507             # Change alphabetical character (ROT13)
508 481 100       1090 if ($c =~ /[a-zA-Z]/) {
509 21         55 $c =~ tr/a-zA-Z/n-za-mN-ZA-M/;
510 21         46 $str .= $c;
511             }
512              
513             # Add numerical value
514             else {
515 460         1285 $str .= '-' . ord($c);
516             };
517             };
518              
519 42         145 return $str;
520             };
521              
522              
523             1;
524              
525              
526             __END__