Tuesday, February 18, 2014

AeroGear iOS lib - 1.4.0 Release

Today, we are happy to announce the immediate availability of 1.4.0 of the AeroGear iOS library. The highlights of this release is the introduction of an Encrypted SQLite store in par with our encrypted In-Memory and Plist based stores, multipart upload API has been enhanced to support more data types apart from the regular NSURL object, and last but not least, an initial preview of our Oauth2 authorization adapter.

Let's dive in..

Encrypted SQLite store

If you have used the encrypted variants of plist and memory stores in our previous versions, you can easily switch to the encrypted SQLite variant by just setting the store type to ENCRYPTED_SQLITE. You can then use the regular Store protocol to read and write data, but this time the data are stored encrypted in an SQLite database. Let's see an example:

NSData *salt = [AGRandomGenerator randomBytes];
 
// set up crypto params configuration object
AGPassphraseCryptoConfig *config = [[AGPassphraseCryptoConfig alloc] init];
[config setSalt:salt];
[config setPassphrase:self.password.text];
 
// initialize the encryption service passing the config
id<AGEncryptionService> encService = [[AGKeyManager manager] keyService:config];
 
// access Store Manager
AGDataManager *manager = [AGDataManager manager];
 
// create store
store = [manager store:^(id<AGStoreConfig> config) {
    [config setName:@"CredentialsStorage"];
    [config setType:@"ENCRYPTED_SQLITE"];
    [config setEncryptionService:encService];
}];
 
// ok time to attempt reading..
NSArray *data = [store readAll])
 
if (data)
    // decryption succeeded!

If you are not familiar with the security API, you can find detailed description in our Cryptography support guide. Further, you can also try our Crypto Cookbook demo that stores your passwords encrypted using either a plist or an sqlite backend.

Enhanced Multipart upload

In the previous version of the library, multipart upload was supported by the mean of attaching an NSURL object on the request that pointed to a local file. Although this method is still supported, we deprecated in favour of being more flexible on the data types that can be attached. Let's see an example usage:

// a multipart that contains a file
NSURL *file1 = [NSURL fileURLWithPath:@"path-to-the-a-local-file"];
AGFilePart *filePart = [[AGFilePart alloc]initWithFileURL:file1 name:@"myfile"];

// a multipart that contains an NSData object
NSData *data1 = [@"Lorem ipsum dolor sit amet.." dataUsingEncoding:NSUTF8StringEncoding];
AGFileDataPart *dataPart = [[AGFileDataPart alloc] initWithFileData:data1 
                                                               name:@"data1"
                                                            fileName:@"data1.txt" mimeType:@"text/plain"];

// set up payload
NSDictionary *dict = @{@"somekey": @"somevalue", 
                       @"another_key": @"some_other_key",
                       @"file1":filePart,
                       @"file2":dataPart};

// set an (optional) progress block
[[apiClient uploadPipe] setUploadProgressBlock:^(NSUInteger bytesWritten, long long totalBytesWritten, long long totalBytesExpectedToWrite) {
    NSLog(@"UPLOADPIPE Sent bytesWritten=%d totalBytesWritten=%qi of totalBytesExpectedToWrite=%qi bytes", bytesWritten, totalBytesWritten, totalBytesExpectedToWrite);
}];

// upload data
[[apiClient uploadPipe] save:dict success:^(id responseObject) {
    NSLog(@"Successfully uploaded!");

} failure:^(NSError *error) {
    NSLog(@"An error has occured during upload! \n%@", error);
}];

As you can see from the example, we introduced new classes to encapsulate the different data types that can be attached on the request. An AGFilePart that can point to local NSURL objects, an AGFileDataPart that can be initialized from an NSData object directly and (not shown) an AGStreamPart that can be initialized from an NSInputStream.

OAuth2 Authorization

New to this release is the introduction of an OAuth2 adapter that will allow you to authorize against OAuth2 protected providers. Let's see an example usage that fetches the list of files residing in your Google Drive:

NSURL* serverURL = [NSURL URLWithString:"@"https://www.googleapis.com/drive/v2"];  
AGPipeline* googleFiles = [AGPipeline pipelineWithBaseURL:serverURL];  

AGAuthorizer* authorizer = [AGAuthorizer authorizer];  

_restAuthzModule = [authorizer authz:^(id<AGAuthzConfig> config) {              
    config.name = @"restAuthMod";  
    config.baseURL = [[NSURL alloc] initWithString:@"https://accounts.google.com"];  
    config.authzEndpoint = @"/o/oauth2/auth";  
    config.accessTokenEndpoint = @"/o/oauth2/token";  
    config.clientId = @"XXXXX";  
    config.redirectURL = @"org.aerogear.GoogleDrive:/oauth2Callback";  
    config.scopes = @[@"https://www.googleapis.com/auth/drive"];  
}];  

[_restAuthzModule requestAccessSuccess:^(id object) {                           
    
    id<AGPipe> files = [googleFiles pipe:^(id<AGPipeConfig> config) {       
        [config setName:@"files"];  
        [config setAuthzModule:authzModule];                                        
    }];  
  
    [files read:^(id responseObject) {                                          
        // responseObject contains the list of files
    } failure:^(NSError *error) {  
        // when an error occurs... at least log it to the console..  
        NSLog(@"Read: An error occured! \n%@", error);  
    }];  

} failure:^(NSError *error) {  
}];

My friend Corinne has already written an excellent two-part (Part1 | Part2) series about the usage of the OAuth2 adapter and I strongly suggest you to visit her blog to learn more. You can also try our Cookbook demos, GoogleDrive and Shoot that go against Google Drive to fetch and store files.


As a final note, this release marks the end of the support of iOS 5/6 versions. Work is under-way to remove any hooks that were made to cater for bugs on those versions and we will focus our efforts exclusively on iOS 7 going forward.

So go ahead and give the release a spin. Download and explore our Cookbook demos, install the Xcode template on your IDE to easily get started, play and have fun!  We will be happy to hear your thoughts, suggestions and ways to improve it better. Join our mailing list, hangout on our #aerogear IRC channel and better, file us bugs!

Wednesday, November 27, 2013

AeroGear iOS lib - 1.3.0 Release

Today, we are happy to announce the immediate availability of 1.3.0 of the AeroGear iOS library. As with our JS and Android supported platforms, main focus of this release was enabling local data encryption. Further, welcome additions such as multipart upload support and new SQLite adapter for our DataManager interface made onto this release.
Let's dive in..

aerogear-crypto-ios
"Crypto for Humans"

Applying cryptographic techniques is hard. iOS platform makes it even harder, firstly by having the functionality dispersed across different frameworks and secondly (and most important), interacting with those frameworks is not the most easy task in the world.  This became clear evidence, as we started to build local data encryption support for our DataManager.  So, we decided to spin off a new iOS library, aerogear-crypto-ios. As stated in the project's web site, main aim is to provide useful and easy to use API interfaces for performing advanced cryptographic techniques in the iOS platform.

Let's see an example on how you can encrypt/decrypt a simple String object:

NSString *stringToEncrypt = @"I want to keep it secret";
// encode string into data
NSData *dataToEncrypt = [stringToEncrypt dataUsingEncoding:NSUTF8StringEncoding];

// set up crypto params..
// generate symmetric key
NSData *encryptionKey = [pbkdf2 deriveKey:@"password4me" salt:[AGRandomGenerator randomBytes]];
// ..and a random Initialisation Vector (IV)
NSData *IV = [AGRandomGenerator randomBytes;

// init the crypto engine
AGCryptoBox *cryptoBox = [[AGCryptoBox alloc] initWithKey:encryptionKey];

// encrypt
NSData *encryptedData = [cryptoBox encrypt:dataToEncrypt IV:IV];

// decrypt
NSData *decryptedData = [cryptoBox decrypt:encryptedData IV:IV];
 

You will appreciate the 'cleanness' of the code. In four lines (excluding the declarations), we derived a PBKDF2 based encryption key, initialised our encryption engine for Symmetric Encryption, and use it to encrypt/decrypt our data.

That is our major aim for our next items to come. Provide as much as possible cleaner interfaces to the underlying crypto functionality offered by the platform (but not limited to), so the developer can stay focus on what is important, an easy-to-easy API to add crypto in their own applications. If you are interested in Crypto and got frustrated by the number of hoops you needed to jump to implement it in your own iOS applications, that's the perfect time to jump in. We love to hear your ideas and suggestions to help shape the project and move forward.

For detailed information on the API, check our iOS cookbook guide. Further, since Christmas is coming, we built a demo application that utilises the crypto library to encrypt your present wishes! My colleague Corinne has already blogged about it, so head to her blog post for more information.

DataManager

Encrypted Plist/Memory

Built on the security foundation provided by the crypto library, this release adds encrypted variants of the existing in-memory and plist based data stores (with an sqlite variant coming next release).  If you have been using the existing data stores to persist your data, you will be happy to know that you can easily switch to an encrypted variant by simple changing the type and some small (really!) amount of code to provide the necessary crypto params.

Let's see a example of using an encrypted plist:

// randomly generate salt
NSData *salt = [AGRandomGenerator randomBytes];

// set up crypto params configuration object
AGPassphraseCryptoConfig *config = [[AGPassphraseCryptoConfig alloc] init];
[config setSalt:salt];
[config setPassphrase:self.password.text];

// initialize the encryption service passing the config
id<AGEncryptionService> encService = [[AGKeyManager manager] keyService:config];

// access Store Manager
AGDataManager *manager = [AGDataManager manager];

// create store
store = [manager store:^(id<AGStoreConfig> config) {
    [config setName:@"CredentialsStorage"];
    [config setType:@"ENCRYPTED_PLIST"];
    [config setEncryptionService:encService];
}];

// ok time to attempt reading..
NSArray *data = [store readAll])

if (data)
    // decryption succeeded!
 

Detailed description of the API can be found on our iOS cookbook guide. In a nutshell, we introduced a new configuration parameter in the store that accepts an encryption service. The store will then use the service to transparently encrypt/decrypt data as they pass in. Currently a default symmetric encryption service based on PBKDF2 is implemented, but we have more plans in the future so stay tuned!

To showcase the crypto store, we built a "Password Manager" that allows a user to store passwords encrypted. Head over to the demo page to try it out yourself or watch a small video demonstrating it in action!


SQLite Adapter

A new adapter has been added on our DataManager that utilises an SQLite database as its backend. Simply specify 'SQLite' when creating a store and you can start using it immediately:

// create the datamanager
AGDataManager* dm = [AGDataManager manager];
// add a new SQLite store object
id<AGStore> = [dm store:^(id<AGStoreConfig> config) {
   [config setName:@"tasks"];
   [config setType:@"SQLITE"];
}];

The read, reset or remove API behave the same, as on our other store types, but this time objects are persisted in the sqlite database.

Don't forget to checkout our 'Recipe' application on our ios-cookbook, for example usage of the SQLite to store your favourite recipes!


Pipeline

Multipart upload

Support has been added on the Pipe interface to perform a multipart upload. No new API has been introduced, but instead we utilise the existing 'save' operation checking if the dictionary passed in contains instances of NSURL objects that point to local files. If that is the case, we kickstart the upload process.

Here is an example snippet, which uploads two images files on a server:

 // the files to be uploaded
 NSURL *file1 = [[NSBundle mainBundle] URLForResource:@"picture1" withExtension:@"jpg"];
 NSURL *file2 = [[NSBundle mainBundle] URLForResource:@"picture2" withExtension:@"jpg"];

 // construct the data to sent with the files added
 NSDictionary *dict = @{@"somekey": @"somevalue", @"jboss.jpg":file1, @"jboss2.jpg":file2 };

 // set an (optional) progress block
 [pipe setUploadProgressBlock:^(NSUInteger bytesWritten, long long totalBytesWritten, long long totalBytesExpectedToWrite) {
    NSLog(@"Sent bytesWritten=%d totalBytesWritten=%qi of totalBytesExpectedToWrite=%qi bytes", bytesWritten, totalBytesWritten, totalBytesExpectedToWrite);
 }];

// upload data
[[pipe save:dict success:^(id responseObject) {
    NSLog(@"Successfully uploaded!");

} failure:^(NSError *error) {
    NSLog(@"An error has occured during upload! \n%@", error);
}];

Note the ‘key’ in the dictionary is used as the ‘name’ field in the multipart request and is required. Further, you don’t need to specify the ‘Content-type’ or the ‘filename’ fields as they are automatically determined internally by the last path component of the NSURL object passed in. If the mime-type can’t be determined an ‘application-octet-stream’ would be sent instead.


To sum up, we think that you will enjoy this release. We have already laid our foundation for crypto, an area we are particularly interested in to moving forward as it adds significant value in the iOS community. So go ahead and give the libraries a spin. Download and explore our demos, play and have fun! We will be happy to hear your thoughts, suggestions and ways to improve it better. Join our mailing list, hangout on our #aerogear IRC channel and better, file us bugs!


Thursday, January 31, 2013

AeroGear iOS lib - Milestone 3

Today we are happy to announce the immediate availability of Milestone 3 of the AeroGear iOS library. Focus on this release was API overhaul cleanups as we prepare for the final release, but that didn't stop us to provide two neat new features that we hope our users will find them useful.

Pagination


The first feature is Pagination support of the result set returned by the server. You now have the ability to scroll either backwards or forward in the result set with the API trying to be flexible enough to support different pagination strategies supported by a server. Paging metadata located in the server response (either in the header or in the body) are used to identify the next or the previous result set. For example, in Twitter case, paging metadata are located in the body of the response, using next_page or previous_page JSON keys at the root level to identify the next or previous result set. The location of this metadata as well as naming, is fully configurable during the creation of the Pipe, thus enabling greater flexibility in supporting several different paging strategies. By default, if no configuration is applied, the library will choose to use Web Linking as it's paging strategy.

Here is an example of pagination that goes against the AeroGear Controller Server:

    NSURL* baseURL = [NSURL URLWithString:@"https://controller-aerogear.rhcloud.com/aerogear-controller-demo"];
    AGPipeline* pipeline = [AGPipeline pipelineWithBaseURL:baseURL];
    
    id<AGPipe> cars = [pipeline pipe:^(id<AGPipeConfig> config) {
        [config setName:@"cars-custom"];
        [config setNextIdentifier:@"AG-Links-Next"];
        [config setPreviousIdentifier:@"AG-Links-Previous"];
        [config setMetadataLocation:@"header"];
        // --optional config option--
        // you have the option to set default parameters that
        // will be passed along during pagination kickstart.
        // If not specified, a default query parameter provider
        // will be installed, that uses the names "offset" and
        // "limit" (two popular names found in pagination mechanisms)
        // initialized to 0 (first page) and 10 (ten items per page) respectively.
        [config setParameterProvider:@{@"color" : @"black", @"page" : @"2", @"per_page" : @10}];        
    }];     
First we create our pipeline. Notice that in the Pipe configuration object, we explicitely declare the name of the paging identifiers supported by the server, as well as the the location of these identifiers in the response. Here we used header to specify that the paging identifiers are to be found in the headers of the response. We can also specify body if the identifiers are to be found in the body of the response (like with Twitter's search), or webLinking (default) if your server follows the Web Linking standard.

To kick-start pagination, you use the method readWithParams of the underlying AGPipe, passing your desired query parameters to the server. You have the option not to specify query parameters (by passing nil), which in that case the pipe will use the default parameter provider set during the pipe creation (with the setParameterProvider config option). Upon successfully completion, the pagedResultSet (an enhanced category of NSMutableArray) will allow you to scroll through the result set:

 __block NSMutableArray *pagedResultSet;
 // fetch the first page
 [cars readWithParams:@{@"color" : @"black", @"offset" : @"0", @"limit" : @1} success:^(id responseObject) {
     pagedResultSet = responseObject;

     // do something

 } failure:^(NSError *error) {
     //handle error
 }];

To move forward or backwards in the result set, you simple call next or previous on the pagedResultSet, passing on the familiar callbacks to handle the response:

 
    // go forward..
    [pagedResultSet next:^(id responseObject) {
        // do something

   // go backwards..
   [pagedResultSet previous:^(id responseObject) {
         // do something
         
     } failure:^(NSError *error) {
         // handle error
   }];
    } failure:^(NSError *error) {
        // handle error
    }];

Note that exception cases like moving beyond last or first page is left on the behaviour of the specific server implementation, that is the library will not try treat it differently. Some servers can throw an error (like Twitter or AeroGear Controller does) by responding with an http error response, or simply return an empty list. The user is responsible to cater for exception cases like this.

We suggest you have a look here and here, two examples that go against the two most popular servers Twitter and Github. Further, if you want a feel on how paging is implemented on the other platforms we support (Android and JS) here is a page that collectively describes the API under those platforms.


Property List Storage


Drived by the need to support long term storage of objects and as an exercise of the AGStore mechanism, a simple Property List based extension was implemented. Familiar methods of reading, saving and removing objects are provided, this time though persisting the changes on the filesystem. Here is a quick example, that stores an One Time Password (OTP) secret in the filesystem so that OTP tokens can be generated later (check AeroGear OTP iOS library and demo for more information on OTP).

    AGDataManager* manager = [AGDataManager manager];
    id<AGStore> plistStore = [manager store:^(id<AGStoreConfig> config) {
       [config setName:@"secrets"];
       [config setType:@"PLIST"];
    }];

Here we initialize the store using a PLIST as type to identify that we want a property list storage and giving up a name that will be also used as the filename.

Now we can simple call the familiar methods to read and save objects in the store:

    // the object to save (e.g. a dictionary)
    NSDictionary *saveOtp = [NSDictionary dictionaryWithObjectsAndKeys:@"19a01df0281afcdbe", @"otp", @"1", @"id", nil];

    // save it
    [plistStore save:saveOtp success:^(id object) {
        // the object has been saved
    } failure:^(NSError *error) {
        //handle error
    }];

    // read it back (note that we pass the ID)
    [plistStore read:@"1" success:^(id object) {
        id readOtp = object;
    } failure:^(NSError *error) {
        //handle error
    }];  

So go ahead and give them a try. We will be happy to hear your thoughts, suggestions and ways to improve it better. Join our mailing list, hangout on our #aerogear IRC channel and better, file us bugs!

Enjoy!

Thursday, December 20, 2012

AeroGear and OTP

If you happen to use online banking systems, certainly you will have come across small security devices that provide you with an extra password during your login process. That is, in addition to your standard username/password combination, you are asked to provide an extra password, the so called "One Time Password" (OTP).  That has two effects a) the bank can verify that you are the actual person making the transaction because of the possession of this device that only you can have, the so called possession factor in the two-factor authentication system and b) prevents replay attacks cause the password is only valid for a limited amount of time. This generation of the OTP password can either be done using a hardware device (hardware token) as we described earlier, or with the help of a mobile application running on a smartphone (software token).

In general, there are two approaches to OTP generation, either Mathematical-algorithm-based or Time-synchronized. The former, as the name suggests uses a complex mathematical algorithm, typically a cryptographic hash function in a hash chain mode, together with a secret key to generate the password.  The latter, takes also into consideration the time, which causes the password to change constantly over a period of time e.g. once per minute, greatly enhancing security. On example of such approach is the Time Based One Time Password (TOTP).

So how OTP is related to the AeroGear project? Well recently, with the amazing work of my fellow developer abstractj, library implementations for both iOS and Android (and soon Javascript) of the OTP standard were introduced to the project. Currently they support only TOTP with SHA1 but work is in progress to add additional support for the other standard OTP algorithm, the event-based HOTP, together with more cryptographic hash functions support SHA-256/512.

So how do you use it?

First, a shared secret needs to be obtained that will be used for the calculation of TOTP. Here we use a static string for the purpose of the tutorial and in our demo we transfer it from the network. In practice, a QRCode encoded image of the secret should be used, so the secret should not travel across the network! In the future we will use encoded images for it.

Here is a snippet of code in the iOS land:
 // the secret key
 NSString *secret = @"B2374TNIQ3HKC446";   
 // initialize OTP  
 AGTotp *generator = [[AGTotp alloc] initWithSecret:[AGBase32 base32Decode:secret]];                       
 // generate token  
 NSString *totp = [generator generateOTP];  

Here is a snippet of code in the Android land:
 // the secret key  
 String secret = "B2374TNIQ3HKC446";
 // initialize OTP  
 Totp generator = new Totp(secret);
 // generate token  
 String totp = generator.now();  

In both cases variable "totp" now holds our token which can be send to the remote authentication server to validate.

Worth noticing is that the Java implementation has the verifier component also implemented, so if you back-end is Java, you can also use the implementation in your server-side back-end to verify totp tokens.

If you are an iOS developer, you can find the library already in the coccoapods. Further a demo application has been created that demos the library in action, so I suggest you have a look. You can find it here.

If you are an Android developer, you can find the library already in maven. Just include it in your project.
<dependency>
    <groupId>org.jboss.aerogear</groupId>
    <artifactId>aerogear-otp-java</artifactId>
    <version>1.0.0.M1</version>
    <scope>compile</scope>
</dependency>
For more in-depth information about OTP and AeroGear, I suggest you to look at the official documentation page on the AeroGear web site here. The page includes nice diagrams showing the flow of the authentication process and will help you to better understand the concept.

So go ahead and give them a try. We will love your feedback and suggestions!

Enjoy!


Tuesday, December 18, 2012

Long Live Open Source!

It has bean a long time since I've last updated this blog. Now that this year will soon come to an end (hopefully the year and not the world!), I want to post a small update of all the things that happened to me this year.

My friends know already the passion I share for JBoss technologies for a long time. Since September, I was given the life opportunity to work for the open source company I love most. In the true OSS spirit, it all started from an open source project of mine. If you follow JBoss development closely, probably you have heard about JBoss Admin, an iOS application that will allow you to remotely manage a JBoss 7 application server.  After announcing it in the development forum, I immediately got an interest from the developers and I was offered the chance to work together.

And here I am. For the past three months, I have been working with an extremely talented group of people, building amazing new technologies that will shake (in a true JBoss spirit!), the land in the mobile space.

My small advice to you. Find an open source project that you love and care, get involved with the community, find areas of work to contribute, spread your passion!. You never know where it can take you..


Wednesday, March 17, 2010

JPA 2 with Hibernate 3.5.0-CR-2

Writing this down so maybe it can help someone that wants to get started with JPA 2 with Hibernate 3.5.0-CR-2 as the underlying persistence framework.

Because you can be lost (At least I did!) to write the plumber code (e.g. maven's pom.xml, log4j.properties, persistence.xml etc) I have prepared a small project that demonstrates just an @OneToOne relation between a Customer and Address. I used H2 as the underlying database(my preferred embedded db of choice) but you can easily change it to use your preferred one(by editing the persistence.xml file).

Unzip the file that you can find here, edit the persistence.xml and change the 'javax.persistence.jdbc.url" property (yeap no more vendor properties!) to point to a folder in your local machine.

And then type the usual commands
mvn clean
mvn compile
mvn exec:java -Dexec.mainClass="gr.forthnet.rd.casper.jpademos.jpademo1.Main"


I am thinking of starting a series of posts as I am getting familiar with JPA 2.

Hope that helps

Thursday, August 06, 2009

Αθλιοι

Για όσους δεν με ξέρουν, μένω σε τουριστική περιοχή και καθημερινά έρχομαι Ηράκλειο οπου βρίσκετε η δουλειά μου. Σήμερα λόγω εκτάκτου, δεν είχα αμάξι να μετακινηθώ.

Σηκώνομαι που λέτε πρωί πρωί και 8:15 βρίσκομαι στην στάση με σκοπό να πάρω το λεωφορείο των 8:30 για Ηράκλειο. Περιμένω μαζί με άλλο κόσμο και 8:35 περνάει το λεωφορείο. Ο οδηγός μας κάνει σήμα οταν μας πλησιάζε "Το άλλο...το άλλο έρχετε απο πίσω.." ΟΚ λέω ας περιμένουμε λίγο ακόμα.

Πάει 8:40:45:50:00:5:10:15 φρίκαρα. Και 20 νάτο το άλλο. Ειχαμε μαζευτεί καμια 15αριά νοματέοι και αρχίζουμε να μπαίνουμε. Και τι να δώ, όλλα τα καθίσματα πιασμένα και κόσμος ουρά στο κέντρο. Στριμωγνόμαστε, σπρωχνόμαστε, να έχω φρικάρει και σκάει μύτη ο ελεγκτής για εισιτήριο. Τα είχα δεί όλα, άρχιζα να φωνάζω ...περιμένετε να γεμίσει το λεωφορείο και αυτούς που θα πάρετε στο δρόμο τους γράφετε στα @@, αυτός τον 8:30 γιατί δεν σταμάτησε που ήταν άδειος.... αυτός να μου λεει παπαριές (τα παραπονά σας στην διοίκηση ....). Στο τσακ ειμουνα να πάρω την τροχαία (απο οτι μου είπε μια κυρία οτι δεν πρόκειτε να κερδίσεις τίποτα, ήταν η ήδια σε φάση που ήρθε η τρόχαια έγιναν συστάσεις αλλά πάλι τα ίδια)

Τελικά έφτασα 10:40 δουλεία απο της 8:00 που ξεκίνησα!

Τι να πώ, μας βλέπανε και οι τουρίστες τι κολοχανίο είμαστε!
Να τους χαιρόμαστε, τουρισμό θέλουμε κατα τα άλλα....ρε δεν πάτε να γα...

Κτέλ Ηρακλείου fail