During development I often use Fiddler, a great tool to monitor http connections on your machine. Every few seconds or so there was an http request with a User-Agent of 'DavClnt' trying to connect to 'HS1', a machine that used to be a Windows Home Server but which no longer exist.

Surely it doesn't really hurt anybody to have a random http call, but I liked to get rid of it.

Fiddler was showing me the process that initiated the request, it was wmplayer.exe or "Windows Media Player"

I first searched the registry to occurrences of 'HS1', even though I had uninstalled the Home Server Connector from the machine, I found a few places with references to the home server. Deleting them however did not help.

Using a different user account, the http requests did not happen, so it must had something to do with my main user account. I deleted all the Windows Media Player settings under AppData but that also did not help.

Then I used Process Monitor, set the filter to Process Name = wmplayer.exe and indeed the process tried to access certain shares on HS1 but I could not find any clues to where the settings for these operations were.

I then searched for files in my user directory that have the string HS1 and found:

C:\Users\username\AppData\Roaming\Microsoft\Windows\Libraries\Videos.library-ms

One of my libraries still had a location on a share on the home server. I removed that location and finally the http requests stopped.


 
Categories: IT Pro

January 19, 2012
@ 10:47 AM

I've been using the Bing Search API for a while on my peter.hahndorf.eu site. The site is totally static, there are no server-side components. So a search had to be implemented in JavaScript. The code was relatively simple and Bing gives you an unlimited number of search queries which is great.

However recently I noted that the API calls didn't return any results at all, while the bing.com site itself returned the results as expected. I looked around a bit online and found out, that the API is not using the exact same data source as the web site and that several other people had problems with their results as well.

So I turned to the other search guys. I had used an older Google API on the server side a while ago but this time I wanted to replace the client side implementation.

I did not just want to place a Google created HTML fragment search box on my site, I wanted to get just the search results via JSON without any Google goo around it.

You can see the working search at peter.hahndorf.eu/search.html

They have something called the 'JSON/Atom Custom Search API', and I will describe how to use it:

To start with, you need a Google account, quiet likely you already have one. Next you need to define your own custom search engine. This allows you to search just your site not the whole web. Go to http://www.google.com/cse/manage/all and create a new search engine, follow their steps and you receive a 'Search engine unique ID' which you later need in your code.

Next you need an API key. Follow the instructions at http://code.google.com/apis/customsearch/v1/getting_started.html#get_account, the API key is the second parameter that you have to provide to Google every time you use the API. The free API plan only allows you 100 queries per day, which is enough for my small site but for bigger sites you may have to pay them.

You can now test your custom search by just putting the following in your browser's address bar:

https://www.googleapis.com/customsearch/v1?key=INSERT-YOUR-KEY&cx=INSERT-YOUR-ENGINE-ID&q=YOUR-SEARCH-TERM&alt=json

You should get a page with search results formatted in JSON.

Building a search page:

Let's start with the html:

<input type="text" id="txtSearchTerm" size="40" />
<button id="btnSearch" style="display:none;">Start Search</button>

<noscript>JavaScript is required for this page.</noscript>

<div id="searchResult"></div>

<div id="output"></div>

<div>
    <a href="#" id="lnkPrev" title="Display previous result page" style="display:none;">Previous</a> <span id="lblPageNumber" style="display:none;"></span> <a href="#" id="lnkNext" title="Display next result page" style="display:none;">Next</a>
</div>

Let's see, we start with an input field and a button. Because without JavaScript our search doesn't work we hide the button and display a message to the user using the 'noscript' tag.

Next we have three divs, the first 'searchResult' is to display a search result summary or an error message. The second one 'output' is for the actual results and the third one is for the navigation to the next or previous pages. The API returns up to 10 records per call and up to 110 records in total. I did not implement a number of links to jump to the result pages directly. I feel the 'Next' button is good enough.

We also need to include our script file and jQuery, I always put these at the end of the page:

<script type="text/javascript" src="jquery.js"></script>

<script type="text/javascript" src="googlesearch.js"></script>

When developing the script you will do a lot testing and may run into the 100 queries per day limit, to work around this I created a local dummy result page. Use the url from before but instead of the &alt=json parameter at the end, use &callback=SearchCompleted. Also make sure that the search term you are using returns more than 10 results.

https://www.googleapis.com/customsearch/v1?key=INSERT-YOUR-KEY&cx=INSERT-YOUR-ENGINE-ID&q=YOUR-SEARCH-TERM& callback=SearchCompleted

now you get a slightly different result which is JSONP, it wraps the JSON data in a JavaScript function call. Save the page into a text file named dummy.js on your development server. Rather than calling Google for the results, we will now use this file.

Create a 'googlesearch.js' file for our logic, start with the document ready function:

$(function ()
{
    $('#btnSearch').show().click(function () { Search($("#txtSearchTerm").val(),0);});
    $('#lnkPrev').click(function () { Search($("#txtSearchTerm").val(),-1); });
    $('#lnkNext').click(function () { Search($("#txtSearchTerm").val(),1);  });
});

We just set up the event handlers for the links and the button which we also unhide, the second parameter 0,-1 and 1 is for paging.

Next create a search function which initiates the API call:

function Search(term, direction)
{
    url = "http://localhost/dummy.js?callback=?";
    $.getJSON(url, '', SearchCompleted);
}

The url points to our dummy file, notice the callback=? part as a parameter. It surely doesn't make any difference in our static file what parameters we call it with but we still need it. It tells the jQuery Ajax magic to treat the result as JSONP and execute it after receiving it. Naming the dummy file *.js tells the web server to send the file content with a Content-Type header of 'application/x-javascript', which is required for JSONP.

So what actually happens here? Just doing an AJAX call for the JSON data from Google does not work because we can only do AJAX calls to our own domain, not a different one like Google.com. To work around this JSONP wraps the data in JavaScript because the <script> tag in html allows the src attribute to point to a different domain. JQuery executes the JavaScript received from the server. It does nothing else than calling our callback function passing in the JSON data as the only parameter. You can see that in our dummy.js file.

The simplest version of the SearchCompleted function would look like this:

function SearchCompleted(response)
{
    var html = "";
    for (var i = 0; i < response.items.length; i++)
    {
        html += response.items[i]. htmlTitle + "<br />";
    }
    $("#output").html(html);
}

We loop through the JSON data and build up an HTML string which we then display in our output div.

Most likely we want to do a bit more with the results, like linking back to the actual page. Look at our dummy.js file to see all the data we get from Google: ] .htmlTitle, .link and .htmlSnippet are the most useful ones.

You can look at my actual implementation to see an example of how to massage the search results before displaying them. Peter.hahndorf.eu/search.html and peter.hahndorf.eu/css/googlesearch.js

Using the real thing

Now that our results look okay, we can switch over to the Google results, we need to change the url in the Search function:

var url = "https://www.googleapis.com/customsearch/v1?key="+ mGoogleApiKey + "&num=10&cx=" + mGoogleCustomSearchKey + "&start=" + startIndex + "&q=" + escape(term) + "&callback=?";

mGoogleApiKey and mGoogleCustomSearchKey are two variables that I set elsewhere with my real values. The start parameter is needed for paging and we again need the callback=? to tell JQuery to do its JSONP magic.

Paging:

So far we always only get the first 10 results. So lets add some basic paging. Normally this include quite a bit of logic, luckily Google provides some help. If you look at the dummy.js file, you see 'nextPage' and 'request' under 'queries'. Here we can see the total results in (response.queries.request[0].totalResults) and the start index for the next page (response.queries.nextPage[0].startIndex). All we have to do is checking whether there is such a value and then unhide the appropriate link and remember the StartIndex value for the next Ajax call.

Again look at peter.hahndorf.eu/css/googlesearch.js for the code.


 
Categories: Web

The special folders in Windows 7 such as 'My Music, My Documents, My Pictures or my Videos' be default point to Folders under the users home directory such as C:\users\username\Documents or C:\users\username\Music.

I have all my music, videos and photos on separate drives but like to point the special folders to these locations.

In the GUI this is pretty straight forward, right click on the special folder in question and then on the 'Location' tab. Just change the path to the new desired path and OK the dialog.

videoprop

However there are quite a few of these special folder I like to change, plus I have to change them for several users on several computers and this every time I reinstall an OS.

The location of the special folders is defined in the registry under the following key:

HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\

There, we have a sub keys: "User Shell Folders" and "Shell Folders", we need to change the information in both places. Here is an example of a batch file to change the locations for Favorites and Videos:

reg.exe ADD "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders" /f /v Favorites /t REG_EXPAND_SZ /d "F:\Users\Joe\Favorites"
reg.exe ADD "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders" /f /v Favorites /t REG_SZ /d "F:\Users\Joe\Favorites"

reg.exe ADD "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders" /f /v "My Video" /t REG_EXPAND_SZ /d "M:\Media\Videos"
reg.exe ADD "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders" /f /v "My Video" /t REG_SZ /d "M:\Media\Videos"

After you run this batch file under the user account for which you want to make the changes, log off that user and back on again and the changes should be in affect.
 
Categories: IT Pro | Tools

December 23, 2011
@ 01:50 PM

In a hostel in Istanbul they had a decent setup with 4 PCs running DeepFreeze 4.2.
At my usually quick check before using one of the PCs I noticed something in Autoruns.exe, the userinit key (HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\Userinit) had a pointer to …\mpk\mpk.exe


So whatever that is, it runs every time a user logs on. I looked in that directory (which had the attributes hidden and system), and also found: mpkview.exe, running that I got the nice UI of the following software: www.refog.com Personal Monitor

It showed me all web sites visited and all keyboard input by all the people who had used the PC today including Gmail passwords and some online banking credentials.


Checking the options of the software revealed that these logs are emailed every 30 minutes to an address which domain is registered by an Istanbul company.

I checked the other three computers and found the exact same key logger installed.

I talked to the owner of the hostel and as expected it wasn’t the hostel itself spying on its guest. It must have been installed by a guest or an ex-employee.

  
I could find out that the software was active for 28 days, the trial they used would have expired after 30 days anyway. But 28 days on four PCs means they got a lot of information. Before I could do any more forensic work, they reinstalled all four PCs.


It turned out Deepfreeze had been installed with an empty password, which means everybody who knows a little about Deepfreeze could just disable it, install the software and then enable it again. They didn’t even have to use Deepunfreeze.


The second mistake they made is that they were using the administrator account which means that any guest could install new software.


While the first two mistakes were made by the IT guy who set up the PCs the third one was done by the hostel:

They kept this whole affair a secret, thinking about their reputation. They should have told all their guests about this so they could change all the passwords they used while using the hostels PCs.


While so far I only found random malicious software on hostel PCs, this was an organized attack by someone who had physical access to the machines.


For me this means I will be even more careful when using public PCs. Not all keyboard logger software is as easy to find as this one though.


 
Categories: Security | Work on the road

This post is about Glimpse 0.82 which is obsolete, for version 0.83+ check the official documentation at getglimpse.com/Help

In version 0.82 the Glimpse guys changed several aspects of the Glimpse package.

In this post I describe how to manually install Glimpse into your ASP.NET 4 web forms web site
without the need of Visual Studio, nuget, MVC or jquery. All you need is ASP.NET 4

First make sure your site in running under ASP.NET 4, you web.config should have

<compilation debug="false" targetFramework="4.0" />

Getting dependencies

Unless you have ASP.NET MVC or Web Matrix installed on your machine, you need a copy of the assembly:

Microsoft.Web.Infrastructure, Version=1.0.0.0

in your bin directory. Glimpse is using it.

It is part of ASP.NET Web Pages:

http://www.microsoft.com/downloads/en/details.aspx?FamilyID=300314DA-DEDD-4540-A236-A0DE0A5A534D&displaylang=en

Download the AspNetWebPages.msi and install it, or if you don't want to install it, extract the files on the command line:

msiexec /a C:\aspnetwebpages.msi /qb TARGETDIR=C:\temp 

You'll find the file "Microsoft.Web.Infrastructure.dll" under the path:

\Microsoft ASP.NET\ASP.NET Web Pages\v1.0\Assemblies

Copy the Microsoft.Web.Infrastructure.dll assembly into the bin directory of your web site.

You may also need the Json.Net Assembly, go to http://json.codeplex.com/ and download the JSON.NET 4.0 Release 2 zip file.

Extract the file: \Bin\Net35\Newtonsoft.Json.Net35.dll into your bin directory.

Get the glimpse Assembly

Download the nuget package manually from:

http://packages.nuget.org/v1/Package/Download/Glimpse/0.82

if you want to use Glimpse with MVC3 you also need this package:

http://packages.nuget.org/v1/Package/Download/Glimpse.mvc3/0.82

(these instructions are tested with version 0.82 only)

Rename Glimpse-0.82.nupkg to Glimpse-0.82.zip and extract the file \lib\net40\Glimpse.Core.dll into your bin directory.

For MVC do the same and copy \lib\net40\Glimpse.Mvc3.dll into you bin directory.

For web forms usage only:

Create a "glimpse" directory in the root of your web site,

Download the following files into that directory:

https://github.com/Glimpse/Glimpse/raw/master/source/Glimpse.Core/glimpseClient.js
https://github.com/Glimpse/Glimpse/blob/master/source/Glimpse.Core/glimpseSprite.png?raw=true
https://github.com/Glimpse/Glimpse/blob/master/source/Glimpse.Core/glimpseLogo.png?raw=true

You should now have the these files in the glimpse directory:

glimpseClient.js
glimpseSprite.png
glimpseLogo.png

Configuration:

Finally you need to add Glimpse to your web.config:

add or integrate into the configSections at the very top of the file (just after <configuration>)

<configSections> 
    <section name="glimpse" type="Glimpse.Core.Configuration.GlimpseConfiguration" /> 
<configSections> 

Add the following section anywhere within <configuration>

<glimpse enabled="true" /> 

Now you should be good to go, enter yoursite.com/glimpse/config in a browser and turn on Glimpse.


 
Categories: ASP.Net

I manage my MP3 collection with my own hand-written tool because I already have a big database with information about the bands and songs.

One of the features of the playlist manager is to specify custom SQL code to select any of the 30,000+ songs in the database for the playlist.

This allows me to have playlists like:

  • Any songs released in 1986
  • Any songs by bands from Sweden before 1993
  • Any songs released on "Sarah Records" on 10" vinyl.
  • Songs by bands added to the database in the last 12 months

You get the idea.

For my next big trip I was thinking of which songs to take along. I only have about 40 Gig space for music on my player, which is enough for roughly 10,000 files.

One idea was to take songs from my all-time favourite bands, but the filter already returns over 10,000 songs and I wanted to add some other tracks as well. So I said, lets take just 50 songs from each of my 250 favourite bands. Which songs? I don't care, they can be random, but I don't want a song twice, regardless of the fact that I have duplicate MP3s because they have been released on multiple albums or compilations. The SQL to do this was not super simple, so that's why I documented here.

To keep things simpler, I first created two views, the first one filters out duplicate song titles:

CREATE VIEW UniqueSongs
AS
SELECT MIN(FileID) AS FileId, Title, BandKey
FROM SoundFiles
GROUP BY Title, BandKey
the second one returns my favourite bands:
CREATE VIEW FavouriteBands
AS
SELECT BandKey FROM Bands WHERE Rank > 8
Now to the main query, which uses a Common Table Expression:
WITH CTE AS
(
SELECT Row_Number() OVER (Partition BY b.BandKey ORDER BY ABS(CAST(CAST(NEWID() AS VARBINARY) AS INT))) As RowNo, 
u.FileId FROM UniqueSongs u INNER JOIN FavouriteBands b ON b.BandKey = u.BandKey
)
SELECT FileName, RootFolder + Location as Location, BandName, Title, Duration 
FROM SoundFiles 
WHERE FileId IN 
(
SELECT FileId FROM CTE WHERE RowNo <= 50
)
The first SELECT gets all the MP3s for my favourite bands,
ABS(CAST(CAST(NEWID() AS VARBINARY) AS INT)))
just returns a random number which we order by, so we get a different set of up to 50 songs each time we run the query.
The really cool part here is:
Row_Number() OVER (Partition BY b.BandKey ORDER BY

We partition our data by the BandKey in a random order and each row gets an new row number one higher than the previous one. For each new band, the row_number is reset to 1.

Here's how that looks for 3 songs per band:

The second SELECT gets the actual data required to build a playlist, the interesting part is in the sub-select in the WHERE clause:

SELECT FileId FROM CTE WHERE RowNo <= 50

Here we use our common table expression from above to get the first 50 fileIDs per band. Remember the RowNo is a continues increasing number, so filtering for anything less 51, gets us the first 50. Cool!


 
Categories: SQL Server

This post is about Glimpse 0.81 which is obsolete, check this post about newer versions.

If you are not using MVC yet, but like to use some of the goodness of glimpse in your web forms sites, read on.

Requirements for the 0.81 version of Glimpse:

- A web forms site using ASP.NET 4
- jquery.js included on any page you want to glimpse into.
- MVC 3 (http://www.asp.net/mvc/mvc3)

Using nuget to add glimpse to your site in Visual Studio

Get nuget at http://nuget.codeplex.com/.

Use the nuget Package Manager Console,

or right click on your site and use the 'Add Library Package Reference'.

Install-Package Glimpse

in the console, or search for Glimpse in the GUI. The glimpse page on the nuget gallery is at
http://nuget.org/List/Packages/Glimpse

Manually add glimpse without Visual Studio

If you can't or don't want to install nuget, you can add Glimpse to your site manually.

Open the url http://packages.nuget.org/v1/Package/Download/Glimpse/0.81 to download the package.
Rename the .nupkg file to .zip and open it, navigate into /lib/net40 and copy the Glimpse.Net.dll
into the bin directory of your site.

Now open your web.config,

add or integrate the config section at the very top of the file (just after <configuration>)

<configSections>
    <section name="glimpse" type="Glimpse.Net.Configuration.GlimpseConfiguration" />
</configSections>
Add the following section anywhere within <configuration>
<glimpse on="true" saveRequestCount="5">
    <ipAddresses>
        <add address="127.0.0.1" /><!--IPv4-->
        <add address="::1" /><!--IPv6-->
    </ipAddresses>
    <contentTypes>
        <add contentType="text/html"/>
    </contentTypes>
</glimpse>

The Client Site Component

For MVC the required client site component is accessed through a route to /glimpse/glimpseclient.js
You could do the same in web forms, but you could also just drop the js file into that location.

Get the JavaScript file from https://github.com/Glimpse/Glimpse/blob/master/source/Glimpse.Net/glimpseClient.js
and save it into /glimpse/glimpseclient.js in your site.

You also want to grab the image at https://github.com/Glimpse/Glimpse/blob/master/source/Glimpse.Net/glimpseSprite.png
and save it as /glimpse/glimpseSprite.png.

Now enable glimpse by opening /glimpse/config on your site and click the 'Turn Glimpse On' button.

Any aspx page that has jQuery included, you should now see the glimpse button in the lower right corner, click it to open the glimpse panel.

Optional:
In your web.config add

<pluginBlacklist>
    <add plugin="Glimpse.Net.Plugin.Mvc.MetaData"/>
    <add plugin="Glimpse.Net.Plugin.Mvc.Binders"/>
    <add plugin="Glimpse.Net.Plugin.Mvc.Execution"/>
    <add plugin="Glimpse.Net.Plugin.Mvc.Routes"/>
    <add plugin="Glimpse.Net.Plugin.Mvc.Views"/>
</pluginBlacklist>


to the <glimpse section, this will hide the MVC extensions which are useless for web forms.

Troubleshooting:

On one of my sites, the /glimpse/config page did not work, that was because I had Firefox's 'Content-Security-Policy' enabled
on the site which does not allow JavaScript to be executed within a html page. Make sure to turn that off for glimpse.

How does it all work?

I asked myself, how the additional glimpse JavaScript gets onto my page, after all I did not make any changes to the web.config
except for the configuration. I look at the source of glimpse reveals that they are using the new ASP.NET 4 feature 'PreApplicationStartMethod'
to add a http module to a site by just dropping an assembly into the bin directory. No changes to the web.config files are required.
Learn more about this at  nikhilk.net


 
Categories: ASP.Net

At a client of mine we recently found out that some of the Scheduled tasks on some Windows 2008 Servers failed without us noticing, because they wouldn't report to the event log which is monitored.

The event log entries that the Task Scheduler write are useless because even though they have the exit code of the task, they are of the informational type regardless of the exit code.

So I wrote a small tool that monitors the "Last Run Result" of the tasks on a local machine and sends an email if the result was unexpected.

To learn more and download this tool, go to the Motash page


 
Categories: IT Pro | Tools

I am traveling a lot and in my case this means with a small backpack on public transport through
third world countries. I do not take a phone or a computer, but occasionally I need to do some IT work.
For this I need a a full development environment with Visual Studio, IIS and SQL Server.

So rather than carrying my own computer, I am using public ones in hotels, hostels or Internet cafes.

On previous trips, most public PCs I encountered were Windows XP and the default user had
administrative rights. Now with the appearance of Vista and Windows 7 with UAC, it will be more likely
that I'm no longer an administrator.

My setup this time is to use a hard drive with a virtual machine created in Virtual Box.
When I find a Windows machine where I can be an administrator, all I need to do is start TrueCrypt
to mount an encrypted volume and then Virtual Box to start the virtual machine.

If I can not get admin access to the machine, but it possible to boot from USB, I can use a portable Linux.
I can use TrueCrypt to mount my same encrypted volume and Virtual Box for Linux to run the same
virtual machine.

For the hardware I choose the SAMSUNG S1 Mini, a 120Gb 1.8" hard drive, they are smaller and lighter
than 2.5" drives but bigger and faster than normal USB memory sticks. I bought two, so I always
have two copies of everything. I keep them in different parts of my luggage too.
Even better would be a USB3 SSD drive, but they are pretty expensive and not many public computers
have USB 3.0 yet.

On the drive I created a FAT32 partition for Linux, I choose 5Gb, but it can be less, even 1GB should be enough.
I created a second partition (NTFS) for the windows tools and the data, it takes the remaining 105 Gb.

In Virtual Box on my Windows machine, I created a new virtual machine to develop on.
I choose a Windows Server 2003, because it needs less resources than Windows 7 or Server 2008.
I used three different virtual hard drives, one for the OS, one for my data and
one for temp stuff. This way I can back up the data drive without having to back up the big system drive all the time. I installed all the usual goodies: VS2010, IIS, SQL-Server, subversion etc.

I created a 50 GB encrypted volume with TrueCrypt to put the virtual hard drive files onto it.

Along with other portable tools, I copied the TrueCrypt executables and Portable Virtual Box for Windows onto the second partition of the S1 drive.

Both TrueCrypt and VirtualBox need admin rights to run, but if I have them, it is pretty quick to
mount the volume and start up my virtual machine.

Setting up the portable Linux:

Download an Ubuntu ISO, other distributions should work as well.

Install Ubuntu using Linux Live USB Creator, make sure to set some space
for Persistence. This feature allows us to store settings and data on the otherwise read-only live-CD.

Install Virtual Box for Linux
Install TrueCrypt for Linux

After booting the Linux you always end up with a user with root access without even logging in.

You may considering changing this by creating a new user and disable the auto login. However I ran into problems doing this and as I don't plan to keep any data on this Linux system, and my VM is secured by TrueCrypt, I may just leave the default auto login enabled.

I added TrueCrypt to the Startup Programs to launch it automatically after boot.
(it is in /usr/bin/) You can also add the data volume to your favorites to get to it quicker.

Before you create a new VM in VirtualBox on Linux, open the preferences dialog and change
the 'Default Machine Folder' to a location on the TrueCrypt volume, otherwise the saved state
of your machine is saved unencrypted on the drive.

Then I created a new machine and attached the existing three hard drives to it.

Saved state: It would be great if you could save the machine state in Linux and then use that under
a Windows hosts, but it turns out that does not work. Even loading the state saved on
a different physical machine may not work.

I have two different VM definitions, one for Linux and one for Windows, but I do share the
three hard drive files. If I have saved the state of a machine on a Linux host, but then move on
and start it up on a Windows host I get the same saved files from the hard drives. I have to test
this more to see how well this works.

What if the machine does not boot from a USB device. Is there a way to have a bootable CD with a bootloader
to tell this machine to load an OS from a USB drive? It turns out there is. There are some instructions on how to create such a CD at pendrivelinux.com.

 


 
Categories: Work on the road