Tweaking Minidlna Media Server on AsusWRT Merlin

usb storage, disable album arts, performance

You’re running AsusWRT Merlin and have some trouble with minidlna, e.g. bad media indexing performance or broken media databases ?

This can be caused by using an USB Stick as media database storage! Internally, minidlna is using an SQLite database to store the media file index – and sometime this database may broke (slow, unsynced file operations, user terminated processes).

As a workaround, it’s possible to move the media database to the temporary filesystem (ramdisk). As a disadvantage, on every system shutdown (reboot/power cycle) the database will be destroyed. But it only takes a view minutes to recreate it, because the ramdisk storage is a lot faster than the attached USB drive!

Just create an additional user config file in your JFFS /jffs/configs/minidlna.conf.add (will be automatically appended to the system generated minidlna.conf file!)

# Move the database to the tmp directory (ramdisk, will be recreated on reboot !!)
db_dir=/tmp/.minidlna

# create a custom minidlna logfile
log_dir=/var/log

# disable album art indexing
album_art_names=NO_ALBUM_ARTS.x

 

Single File ReCaptcha 2 PHP Client

leading captcha system, curl, php, json-response

Today, a web-form without a proven captcha system generates a lot of spam entries and data-trash in your database. One of the best is ReCaptcha (even the latest v2).

Google provides an easy to use ReCaptcha PHP Client – but it’s a bit over engineered! You need a bunch of PHP files and a composer based environment to use it out of the box. This can cause some trouble in highly customized/optimized projects.

Therefore, here is a “one-file” solution which works without any configuration overhead:

Usage#

require('ReCaptcha.php');

// register your secret
ReCaptcha::setSecret('<your-secret>');

// some code ...

// check user form
if (ReCaptcha::isValid()){ ...

One-File Solution#

// Developer Guide: https://developers.google.com/recaptcha/docs/verify
class ReCaptcha{

    // ReCaptcha API Endpoint
    const SITE_VERIFY_URL = 'https://www.google.com/recaptcha/api/siteverify';

    // the last result
    private static $_result = null;

    // client secret
    private static $_secret = null;

    // validate
    public static function isValid(){
        // token available ?
        if (!isset($_POST['g-recaptcha-response'])){
            return false;
        }

        // extract token
        $token = trim($_POST['g-recaptcha-response']);

        // generate url
        $params = http_build_query(array(
            'secret' => self::$_secret,
            'response' => $token,
            'remoteIp' => $_SERVER['REMOTE_ADDR']
        ), '', '&');

        // create curl based post request
        $handle = curl_init(self::SITE_VERIFY_URL);
        $options = array(
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => $params,
            CURLOPT_HTTPHEADER => array(
                'Content-Type: application/x-www-form-urlencoded'
            ),
            CURLINFO_HEADER_OUT => false,
            CURLOPT_HEADER => false,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_SSL_VERIFYPEER => true
        );
        curl_setopt_array($handle, $options);
        $response = curl_exec($handle);
        curl_close($handle);

        // decode response
        self::$_result = json_decode($response, true);

        // check
        return (self::$_result['success'] === true);
    }

    // error occurred ?
    public static function isError(){
        return (self::$_result['success'] === false);
    }

    // get error message from last request
    public static function getErrorMessages(){
        return self::$_result['error-codes'];
    }

    // set client secret
    public static function setSecret($s){
        self::$_secret = $s;
    }
}

 

Node.js Simple Command Line Confirm Messages

user confirmation, terminal actions, yes, no

Sometime, special terminal commands can be dangerous for your users. To ensure that they are really want to run the command the proven “best practise” is to wait for an explicit user confirmation by typing yes/no into the terminal.

Install “prompt” using NPM#

First of all, you have to install prompt – a powerfull package for command line prompts & user interactions. The “–save” option will add this package to your package.json file.

npm install prompt --save

Confirm Dialog#

After installing the prompt package you can use the following code to show a confirm dialog to your users.

var _prompt = require('prompt');

// user confirmation required!
_prompt.start();

// disable prefix message & colors
_prompt.message = '';
_prompt.delimiter = '';
_prompt.colors = false;

// wait for user confirmation
_prompt.get({
    properties: {
        
        // setup the dialog
        confirm: {
            // allow yes, no, y, n, YES, NO, Y, N as answer
            pattern: /^(yes|no|y|n)$/gi,
            description: 'Do you really want to format the filesystem and delete all file ?',
            message: 'Type yes/no',
            required: true,
            default: 'no'
        }
    }
}, function (err, result){
    // transform to lower case
    var c = result.confirm.toLowerCase();

    // yes or y typed ? otherwise abort
    if (c!='y' && c!='yes'){
        console.log('ABORT');
        return;
    }
    
    // your code
    console.log('Action confirmed');
    
});

 

Maybe your wondering about some HTTP Headers sent to your browser which are not set in your script by the header() function ? Especially these Cache-Control headers:

Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache

First of all: everything is ok! These headers are automatically set by the PHP Session module to prevent browser/proxy based caching of your pages. Depending on your environment setup, it’s possible to control these headers by using the session_cache_limiter() function or use the php.ini

To disable these behaviour just pass an empty string to the session_cache_limiter() function as mentioned in the documentation:

Solution#

// add this line to the beginning of your php script to disable the cache limiter funktion:
session_cache_limiter('');

MooTools: A modern Element.highlight() implementation

using CSS3 Transisitons instead of Fx.Tween

Use the following code as Element.highlight() replacement:

Element.implement({
    /**
     * Custom Element Highlighting Function
     * @param color
     */
    highlight: function(color){
        // get current background color
        var originalColor = this.getStyle('background-color');

        // set new background color
        this.setStyle('background-color', color);

        // restore background color after 300ms
        (function(){
            this.setStyle('background-color', originalColor);
        }).delay(300, this);
    }
});

It will only change the background color to the given value and reverse this after a time of 300ms. To get a fading-effect you need to add the following css, matching the elements you wish to apply the highlight() method:

.autocomplete-input{
    transition: background-color 200ms;
}

That’s it ;)

Are you using a Yubikey and want to create your custom Keyserver written in Node.js ? In this case this piece of code might be useful :)

/**
 * Convert the Yubico MODHEX encoded Strings to hex
 * @param modhex String
 * @returns hex String
 */
var modhex2hex = function(modhex){
    // strip whitespaces and string cleanup - all non matching characters are 0x00 (c in modhex)
    modhex = modhex.replace(/\s*/g, '').replace(/[^cbdefghijklnrtuv]/g, 'c');

    // even length ?
    if (modhex.length%2 !== 0){
        return null;
    }

    // modhex mapping base; c.....v => 0x0 ... 0xF
    var modhexBase = 'cbdefghijklnrtuv'.split('');

    // tmp
    var output = '';

    // convert
    for (var i=0;i<modhex.length;i++){
        // convert index to hex
        output += modhexBase.indexOf(modhex.charAt(i)).toString(16);
    }

    return output;
};

console.log(modhex2hex('te vt hh fg ue dk gv rt lv hb lu gf nk ge ng cv'));

Bootstrap Dismissible alerts with MooTools

use .alert-dismissible without jQuery

Bootstrap & MooTool#

Using Dismissible alerts with pure MooTools code. Just insert the following code within your domready startup:

Alert.js Replacement#

// get all elements with the .alert-dismissible class
document.getElements('.alert-dismissible').each(function(el){
  // add a onclick event to each element
  el.getElement('button.close').addEvent('click', function(){
    // hide the element on click
    el.setStyle('display', 'none');
  });
});

Dismissible alerts Example#

<div class="alert alert-danger alert-dismissible" id="alert">
    <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
    <strong>Invalid Regex Rule: </strong> <code class="txt"></code>
</div>

Install Sophos UTM Essential Firewall 9.3 with an USB Stick

plug ‘n play ? not really… (version 9.308-16.1)

First of all, you need to download the iso image (it’s required to create an account – your license key will be sent to this email address).

Install the ISO Image to an USB Drive#

It’s not possible to copy the image via dd directly to the usb drive because of a missing usb bootloader. Therefore you need external bootloader like Universal-USB-Installer from pendrivelinux.

Download and execute the tool.

  • Step 1 choose “Try unlisted Linux ISO” from the bottom of the page.
  • Step 2 select the iso image e.g. asg-9.308-16.1.iso
  • Step 3 select your usb drive (at least 2GB)

After the installation has finished, remove and reattach the usb stick from your system to process with the next step.

Fix the local package repository#

The current (9.308-16.1) iso image has some broken filenames within the local package repository. The files are located under “install/rpm/”. Check each file if it ends with “.rb1.i686.rpm” – if not, modify the filename and add the missing part!

Otherwise the installation will abort and message like “can’t stat file /install/install/rpm/….. rb1.i686.rpm” will appear. I hope this issue will be fixed soon…

Now you can attach the usb stick to your firewall-box and power it up!

Remount the USB Drive#

Ready ? not really… the hardware-detection of the installer will unmount the usb stick, which cause the error message “install.tar not found”. Therefore you have to remount the usb-stick after the hardware detection succeed.

  • After Hardware Detection succeed press “ok”
  • Select the Keyboard Layout and Timezone
  • Press “Alt+F2” to switch to the console
  • Re-Mount the USB drive: mount /dev/sdb /install
  • Press “Alt+F1” to return to the installer
  • Proceed the installation

 

Google Universal Analytics provides a great possibility to track custom events on your website. These feature can be used to analyze special user-interactions with e.g. advertised/highlighted content or social media links. The recorded data can be directly used to optimize your website!

The event tracking is very simple:

To simplify the integration you can implment a simple data-binding which will automatically add event listeners (click) to you DOM elements. Just add an additional attribute to your elements which includes (in this example) a prefix, the event-category and the event-label (scheme: "prefix:category:label").

Example: Social Links with data-binding#

<a href="https://www.facebook.com/groups/" title="Facebook" data-event="ga:social:facebook" class="FacebookButton"></a>
<a href="https://twitter.com/" title="Twitter Stream" data-event="ga:social:twitter" class="TwitterButton"></a>

Finally, we need some javascript to process the additional attributes. You should always check if Google Analytics is enabled (maybe an adblocker is used or an opt-out option is set!) to avoid javascript errors. The following code will send an event to analytics each time a user clicks on the bound elements.

MooTools based Data-Binding#

window.addEvent('domready', function(){
  // GA loaded ? Used to avoid js errors on blocked analytics
  if (typeof ga !== 'function'){
    return;
  }
  
  // event binding
  document.getElements('[data-event]').each(function(el){
    // get event string
    var d = el.get('data-event').split(':');
    
    // format:  "type:category:name"
    if (d.length != 3){
      return;
    }
    
    // extract vars
    var evtType = d[0];
    var evtCategory = d[1];
    var evtName = d[2];
    
    // outgoing link on click event
    el.addEvent('click', function(){
      // universal analytics event ?
      if (evtType == 'ga'){
        ga('send', 'event', evtCategory, 'click', evtName);
      }
    });		
  });
});

 

Simple “Zen” Reading Mode with MooTools

simplifying website headers on-demand

Gigantic, image-based headers are an up-to-date web-feature. It looks amazing but they are squandering space, especially on sites with large tutorials containing long text.

This site is using a simply method which transforms the header-size depending on the current page-scroll: the users scrolls down and the header is automatically transformed to a smart toolbar – i’m calling this mode (following WordPress’ fullscreen editor) “Zen” reader mode. Need a demo ? Just open an article on this site and scroll down.

To integrate this feature into you website, you need the followindd things:

  • “Standard” Header Design
  • “Smart/Compact” Header Design
  • Javascript Page-Scroll Observer
  • Optional: Amazing css3 transition

Different Header-Styles (using LESS):#

/* Basic (default) Styling */
.navbar-beyond{
  height: 70px;
  .transition(500ms);
}

/* Smart Styling */
.navbar-beyond-light{		
  height: 45px;
  border-bottom: solid 1px #e7e7e7;	
  background-color: @color_bluegrey1 !important;
}

/* Large Navbar */
.navbar-beyond-dark{
  background-color: rgba(0,0,0,0.85);
  border-bottom: none;
}

Scroll-Observer Script (MooTools) :#

Description: user scrolls 200px down and the header is transformed by changing the css classes

var head = document.getElement('header .navbar-beyond');
... 
window.addEvent('scroll', function(){
  // get current scroll
  var s = this.getScroll().y;
        
  // dark/light topnav
  if (s > 200){
    head.addClass('navbar-beyond-light');
    head.removeClass('navbar-beyond-dark');
  }else{
    head.addClass('navbar-beyond-dark');
    head.removeClass('navbar-beyond-light');
  }
});

Header Structure (Bootstrap based example from this website)#

<!-- Fixed navbar -->
<div class="navbar navbar-default navbar-fixed-top navbar-beyond navbar-beyond-dark" role="navigation">
  <div class="container container-lg">
    <!-- Website Title + Mobile Nav Button !-->
    <div class="navbar-header">
      <button type="button" class="navbar-toggle">
        <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span>
      </button>
      <a class="navbar-brand" href="http://andidittrich.com">Beyond Technology<br /><span>beyond the visible world</span></a>
    </div>
    
    <!-- Navi !-->
    <div class="navbar-collapse hidden-xs">
      <ul id="menu-topnavi" class="nav navbar-nav navbar-right">
      ....
      </ul>
    </div>
  </div>
</div>

<!-- Image -->
<div id="HeaderImage">
</div>