Loading text file by AJAX call in UIWebView using custom NSURLProtocol fails - ios

I want to display a website on an iOS app using a UiWebView. Some components of the site (namely the webservice results loaded using AJAX calls) should be replaced by local data.
Consider the following example:
text.txt:
foo
page1.html:
<html><head>
<title>test</title>
<script type="text/javascript" src="jquery.js"></script>
</head>
<body>
<div id="target"></div>
<script type="text/javascript">
function init(){
$.get("text.txt",function(text){
$("#target").text(text);
});
}
$(init);
</script>
</body></html>
View Controller:
#interface ViewController : UIViewController <UIWebViewDelegate>
#property (nonatomic,assign) IBOutlet UIWebView *webview;
#end
#implementation ViewController
#synthesize webview;
//some stuff here
- (void)viewDidLoad
{
[super viewDidLoad];
[NSURLProtocol registerClass:[MyProtocol class]];
NSString *url = #"http://remote-url/page1.html";
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]];
[request setCachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData];
[webview loadRequest:request];
}
#end
MyProtocol:
#interface MyProtocol : NSURLProtocol
#end
#implementation MyProtocol
+ (BOOL) canInitWithRequest:(NSURLRequest *)req{
NSLog(#"%#",[[req URL] lastPathComponent]);
return [[[req URL] lastPathComponent] caseInsensitiveCompare:#"text.txt"] == NSOrderedSame;
}
+ (NSURLRequest*) canonicalRequestForRequest:(NSURLRequest *)req{
return req;
}
- (void) startLoading{
NSLog(#"Request for: %#",self.request.URL);
NSString *response_ns = #"bar";
NSData *data = [response_ns dataUsingEncoding:NSASCIIStringEncoding];
NSURLResponse *response = [[NSURLResponse alloc] initWithURL:[self.request URL] MIMEType:#"text/plain" expectedContentLength:[data length] textEncodingName:nil];
[[self client] URLProtocol: self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
[[self client] URLProtocol:self didLoadData:data];
[[self client] URLProtocolDidFinishLoading:self];
[response release];
}
- (void) stopLoading{
NSLog(#"stopLoading");
}
#end
If I don't register my custom URLProtocol the page is displayed properly. If I do startLoading() is called, the content is loaded and stopLoading() is triggered afterwards. But on the UIWebView nothing happends at all. I tried to do some error-handling, but neither is a JS AJAX error thrown nor is didFailLoadWithError of the UIWebViewDelegate called.
I tried another scenario and created a HTML page that just loads an image:
<img src="image.png" />
and modified my URLProtocol to just handle the loading of the image - this works properly. Maybe this has anything to do with AJAX calls?
Do you have any idea what the problem might be?
Thanks in advance!

I had the same problem and finally solved it after days of hair pulling :
Your problem comes from the way you create the response, you have to create a status 200 response and force the WebView to allow cross-domain request if necessary :
NSDictionary *headers = #{#"Access-Control-Allow-Origin" : #"*", #"Access-Control-Allow-Headers" : #"Content-Type"};
NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:request.URL statusCode:200 HTTPVersion:#"1.1" headerFields:headers];
You can see my full working implementation in my answer here :
How to mock AJAX call with NSURLProtocol?
Hope this helps,
Vincent

Related

MPMoviePlayerController and Auth-Based HLS Backend Server

I am currently serving videos in my iOS application with MPMoviePlayerController. The files are streamed from our backend server that requires authentication. It is key-based authenticated set in the Authorization HTTP Header.
It used to work perfectly with single video files. Now we’re trying to implement HLS adaptive streaming and we have faced a wall. I am currently using a custom NSURLProtocol subclass to catch requests made to our backend server and inject the proper Authorization header. For HLS it simply doesn’t work.
When we looked at the server logs, we clearly saw that the first request to the m3u8 file worked fine. Then all subsequent calls made (other m3u8 files and ts also) are 403 forbidden. It seems that MPMoviePlayerController doesn’t use NSURLProtocol for the other files. (side note: It does work on the simulator thought, but not on a physical device which let me think that both are not implemented in the same way).
MPMoviePlayerController instantiation
self.videoController = [[MPMoviePlayerController alloc] initWithContentURL:video.videoURL];
The URL Protocol interception
+ (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *)request {
NSMutableURLRequest *newRequest = request.mutableCopy;
[newRequest setValue:#"HIDDEN" forHTTPHeaderField:#"Authorization"];
return newRequest;
}
Any Ideas, suggestions, work arounds?
After verification with Apple Developer Technical Support, I figured what I wanted to achieve is impossible (and unsupported).
Here's the quote from the reply :
The problem you're seeing with NSURLProtocol and so on is that the movie playback subsystem does not run its HTTP requests within your process. Rather, these requests are run from within a separate system process, mediaserverd. Thus, all your efforts to affect the behaviour of that playback are futile.
By using NSURLProtocol, you can intercept the communication between MPMoviePlayerController and the streamed requests. To inject cookies along the way, or possibly save the stream offline videos. To do this, you should to create a new class extending NSURLProtocol:
Hope this helps you:
GAURLProtocol.h
#import <Foundation/Foundation.h>
#interface GAURLProtocol : NSURLProtocol
+ (void) register;
+ (void) injectURL:(NSString*) urlString cookie:(NSString*)cookie;
#end
GAURLProtocol.m
#import "GAURLProtocol.h"
#interface GAURLProtocol() <NSURLConnectionDelegate> {
NSMutableURLRequest* myRequest;
NSURLConnection * connection;
}
#end
static NSString* injectedURL = nil;
static NSString* myCookie = nil;
#implementation GAURLProtocol
+ (void) register
{
[NSURLProtocol registerClass:[self class]];
}
// public static function to call when injecting a cookie
+ (void) injectURL:(NSString*) urlString cookie:(NSString*)cookie
{
injectedURL = urlString;
myCookie = cookie;
}
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
if([[[request allHTTPHeaderFields] objectForKey:#"Heeehey"] isEqualToString:#"Huuu"])
{
return NO;
}
return [[[request URL] absoluteString] isEqualToString:injectedURL];
}
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
return request;
}
// intercept the request and handle it yourself
- (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id<NSURLProtocolClient>)client {
if (self = [super initWithRequest:request cachedResponse:cachedResponse client:client]) {
myRequest = request.mutableCopy;
[myRequest setValue:#"Huuu" forHTTPHeaderField:#"Heeehey"]; // add your own signature to the request
}
return self;
}
// load the request
- (void)startLoading {
// inject your cookie
[myRequest setValue:myCookie forHTTPHeaderField:#"Cookie"];
connection = [[NSURLConnection alloc] initWithRequest:myRequest delegate:self];
}
// overload didReceive data
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[[self client] URLProtocol:self didLoadData:data];
}
// overload didReceiveResponse
- (void)connection:(NSURLConnection*)connection didReceiveResponse:(NSURLResponse *)response {
[[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:[myRequest cachePolicy]];
}
// overload didFinishLoading
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
[[self client] URLProtocolDidFinishLoading:self];
}
// overload didFail
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
[[self client] URLProtocol:self didFailWithError:error];
}
// handle load cancelation
- (void)stopLoading {
[connection cancel];
}
#end
Register
// register protocol
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[NSURLProtocol registerClass:[GAURLProtocol class]];
return YES;
}
Usage
[GAURLProtocol injectURL:#"http://example.com/video.mp4" cookie:#"cookie=f23r3121"];
MPMoviePlayerController * moviePlayer = [[MPMoviePlayerController alloc]initWithContentURL:#"http://example.com/video.mp4"];
[moviePlayer play];
#Marc-Alexandre Bérubé I can think of below workaround:
Run a proxy server in your app to proxy all the video URL's. Download all the video content by injecting the necessary auth headers to the request and relay back the content via the proxy server to the media player to render it. This approach may not work for large videos as the video rendering would only start after entire video is downloaded.

How to load URL in Uiwebview with GameCenter data as parameters?

I have a browser webgame ported to iOS using UIWebView (storyboard, not xib). The app is already published and works relatively well. My challenge now is to implement GameCenter properly so I can get the local player's playerID and then have a trustable unique id of the user. That's the only feature I really need from GameCenter.
The GameCenter functionality is actually implemented and working well, but for a reason I still couldn't understand I can't make the setAuthenticateHandler, located in a different class file, to access the UIebView so I can load an URL passing the playerId (among other parameters) via POST.
The WebViewController.m has a method who loads an URL in the UIWebView. The setAuthenticateHandler do call this method, but the UIWebView is null and of course it's not possible to load the URL. If I call the method inside WebViewController.m the URL is loaded correctly.
I was using ARC and then changed to MRC according to https://stackoverflow.com/a/14613361/3063226, but the behavior didn't change, the UIWebView is still null when GameCenter setAuthenticateHandler calls the Load_URL method.
Any help is very welcome. I have tried and researched a LOT for days before coming here to ask a question, I'm stuck!
GCHelper.m
- (void)authenticateLocalUser {
if (!gameCenterAvailable) return;
NSLog(#"Authenticating local user...");
if (![GKLocalPlayer localPlayer].isAuthenticated) {
[[GKLocalPlayer localPlayer] setAuthenticateHandler:(^(UIViewController* viewcontroller, NSError *error) {
if (!error && [GKLocalPlayer localPlayer].playerID != nil)
{
NSLog(#"Player logged in GameCenter!");
NSLog([NSString stringWithFormat: #"1111 (GCHelper) playerID: [%#]", [GKLocalPlayer localPlayer].playerID);
// --
WebViewController *yourClassInstance = [[WebViewController alloc] init];
[yourClassInstance Load_URL:true]; // this is not working as expected!!
// -
}
else
{
NSLog(error.localizedDescription);
}
})];
} else {
NSLog(#"Already authenticated!");
}
}
WebViewController.h
#import <UIKit/UIKit.h>
#interface WebViewController : UIViewController <UIWebViewDelegate>
-(void) Load_URL:(BOOL)Nativa;
#property (strong, nonatomic) IBOutlet UIWebView *PagPrincipal;
#end
WebViewController.m
#import "WebViewController.h"
#import "NSData+AESCrypt.h"
#import "NSString+AESCrypt.h"
#import "NSString+URL_Encode.h"
#import "GCHelper.h"
#interface WebViewController ()
-(void) Load_URL:(BOOL)Nativa;
#end
-(void) Load_URL :(BOOL)Nativa
{
NSURL *url = [NSURL URLWithString: #"http://www.webgameurl.com"];
NSString *body = [NSString stringWithFormat: #"iac=%#", [self URL_Codigo :Nativa :vip_aux]];
NSMutableURLRequest *requestObj = [[NSMutableURLRequest alloc]initWithURL: url];
[requestObj setHTTPMethod: #"POST"];
[requestObj setHTTPBody: [body dataUsingEncoding: NSUTF8StringEncoding]];
// HERE!! This returns null when called from GCHelper.m, but is ok when called from WebViewController.m !!
NSLog([NSString stringWithFormat:#"Webview >>> >>> >>> [%#]", _PagPrincipal.description]);
_PagPrincipal.delegate = self;
[_PagPrincipal loadRequest:requestObj];
This is it. The _PagPrincipal is the UIWebView itself, it's a storyboard. I'm not an iOS Objective-C specialist, so for sure there are things I just don't master. Using Xcode 6.1.1, the app is designed for iOS8+. Testing in a real iPad Mini 1 (non-retina) and iPhone 5.

Subclassing NSURLConnection gives error: unrecognized selector sent to instance

i'm trying to make a subclass of NSURLConnection where i have an additional property (in this case "connectionName") to help me distinguish between 2 different connections.
i created the subclass, named it CustomURLConnection and gave it the property "connectionName".
then in my file ImagesViewController.m (which is an UICollectionView) i import the header CustomURLConnection and try to give the connections a name and retrieve it afterwards, but it doesn't work, as soon as i enter this collection view the app crashes and gives me the following error:
-[NSURLConnection setConnectionName:]: unrecognized selector sent to instance 0x1090a40f0
Here is some code: (if you want, here's a CLEARER IMAGE)
CustomURLConnection.h
#import <Foundation/Foundation.h>
#interface CustomURLConnection : NSURLConnection
#property (strong, nonatomic) NSString *connectionName;
#end
ImagesViewController.h
#import <UIKit/UIKit.h>
#interface ImagesViewController : UICollectionViewController<NSURLConnectionDelegate>
#property (strong, nonatomic) UIImageView *imageView;
#end
ImagesViewController.m
...
#import "CustomURLConnection.h"
#interface ImagesViewController (){
NSArray *contentStrings;
NSMutableData *contentData; // Holds data from the initial load
NSMutableData *contentImageData; // Holds data when the user scrolls up/down in the collection view
}
#end
...
-(void)loadInitialData{ // Loads data from page
NSString *hostStr = #"http://www.website.com/example";
NSURL *dataURL = [NSURL URLWithString:hostStr];
NSURLRequest *request = [NSURLRequest requestWithURL:dataURL];
CustomURLConnection *connectionData = (CustomURLConnection *)[NSURLConnection connectionWithRequest:request delegate:self]; // Make connection
connectionData.connectionName = #"InitialData"; // Give it a name
}
...
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
// Do some stuff
NSString *hostStr = #"http://www.website.com/example2";
_imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0,0,100,100)];
[imageCell addSubview:_imageView]; // Adds an image view to each collection cell
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:hostStr]];
CustomURLConnection *connectionImg = (CustomURLConnection *)[NSURLConnection connectionWithRequest:request delegate:self]; // Make connection
connectionImg.connectionName = #"ImageData"; // Give it a different name than before
// Do some stuff
return imageCell;
}
...
// Here are the main methods for the connections
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{
if([((CustomURLConnection *)connection).connectionName isEqualToString:#"InitialData"]){
contentData = [[NSMutableData alloc] init];
}
else{
contentImageData = [[NSMutableData alloc] init];
}
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
if([((CustomURLConnection *)connection).connectionName isEqualToString:#"InitialData"]){
[contentData appendData:data];
}
else{
[contentImageData appendData:data];
}
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection{
if([((CustomURLConnection *)connection).connectionName isEqualToString:#"InitialData"]){
// Do some stuff
}
else{
UIImage *image = [[UIImage alloc] initWithData:contentImageData];
_imageView.image = image;
}
}
am i missing something? i came across this error many times before but the causes are never the same and this time i can't seem to find a solution on my own.
hopefully you can see what i'm doing wrong and help me :)
thanks.
EDIT: turns out there is a better way to achieve my goal, have a look here
Thank again to everyone for the help :)
CustomURLConnection *connectionImg = (CustomURLConnection *)[NSURLConnection connectionWithRequest:request delegate:self]; // Make connection
creates an NSURLConnection object. Casting to CustomURLConnection does not change
the class of this object. Replace that line with
CustomURLConnection *connectionImg = [CustomURLConnection connectionWithRequest:request delegate:self]; // Make connection
to create an instance of your subclass.
In your delegate methods change NSURLConnection by CustomURLConnection, for instance :
- (void)connection:(CustomURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
and when you create it just do :
CustomURLConnection *connectionImg = [[CustomURLConnection alloc] initWithRequest:request delegate:self];
connectionImg.connectionName = #"ImageData"; // Give it a different name than before
In this line:
CustomURLConnection *connectionData = (CustomURLConnection *)[NSURLConnection connectionWithRequest:request delegate:self];
you are creating an instance of NSURLConnection, not CustomURLConnection. So, when you cast the result to CustomURLConnection * you are lying to the compiler.
Later, at runtime, when you try to use a feature of CustomURLConnection you get an exception because your connection is the wrong class type and doesn't implement the method.
You need to instantiate CustomURLConnection, not NSURLConnection.
Adding to the other good answers here, your CustomURLConnection class should override +connectionWithRequest:delegate: to return an instance of CustomURLConnection, like this:
+(CustomURLConnection*)connectionWithRequest:(NSURLRequest*)request delegate:(id)delegate
{
return [[CustomURLConnection alloc] initWithRequest:request delegate:delegate];
}
That lets you use the same style you had:
CustomURLConnection *connectionData = [CustomURLConnection connectionWithRequest:request delegate:self]; // Make connection
More importantly, a user of your code (most likely the future you) might assume that sending +connectionWithRequest:delegate: to CustomURLConnection would return an instance of CustomURLConnection. Without the override, they'll get an instance of NSURLConnection instead, and that's a difficult bug to spot.

iOS POST to Google Froms (Spreadsheet)

I am trying to set up a very simple app, that takes in a number from an input field, and then POSTS using an NSMutableURLRequest to a Google Spreadsheet, using the action URL from the Google Form.
So far I have a simple text box and button, and this code. The field name I am using is:
name="entry.931001663"
#import "ViewController.h"
#interface ViewController ()
#property (weak, nonatomic) IBOutlet UITextField *inputField;
- (IBAction)submit:(id)sender;
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (IBAction)submit:(id)sender {
NSString *bodyData = self.inputField.text;
NSLog(#"Input = %#", bodyData);
NSMutableURLRequest *postRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:#"https://docs.google.com/forms/d/1rGpFfI2ebyn_SbuDVVUg7Q4yuvKzd3RRXb0vRxeIDxc/formResponse"]];
[postRequest setValue:#"application/x-www-form-urlencoded" forHTTPHeaderField:#"Content-Type"];
[postRequest setHTTPMethod:#"POST"];
[postRequest setHTTPBody:[NSData dataWithBytes:[bodyData UTF8String] length:strlen([bodyData UTF8String])]];
}
#end
I am not sure about the error, but Google's Sample Code can help you. You can also use GData Objective C Client to use it.

how to unit testing AFNetworking request

i am making a GET request to retrieve JSON data with AFNetworking as this code below :
NSURL *url = [NSURL URLWithString:K_THINKERBELL_SERVER_URL];
AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:url];
Account *ac = [[Account alloc]init];
NSMutableURLRequest *request = [httpClient requestWithMethod:#"GET" path:[NSString stringWithFormat:#"/user/%#/event/%#",ac.uid,eventID] parameters:nil];
AFHTTPRequestOperation *operation = [httpClient HTTPRequestOperationWithRequest:request
success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSError *error = nil;
NSDictionary *JSON = [NSJSONSerialization JSONObjectWithData:responseObject options:NSJSONReadingAllowFragments error:&error];
if (error) {
}
[self.delegate NextMeetingFound:[[Meeting alloc]init] meetingData:JSON];
}
failure:^(AFHTTPRequestOperation *operation, NSError *error){
}];
[httpClient enqueueHTTPRequestOperation:operation];
the thing is i want to create a unit test based on this data, but i dont want that the test will actually make the request. i want a predefined structure will return as the response. i am kind'a new to unit testing, and poked a little of OCMock but cant figure out how to manage this.
Several things to comment about your question.
First of all, your code is hard to test because it is creating the AFHTTPClient directly. I don't know if it's because it's just a sample, but you should inject it instead (see the sample below).
Second, you are creating the request, then the AFHTTPRequestOperation and then you enqueue it. This is fine but you can get the same using the AFHTTPClient method getPath:parameters:success:failure:.
I do not have experience with that suggested HTTP stubbing tool (Nocilla) but I see it is based on NSURLProtocol. I know some people use this approach but I prefer to create my own stubbed response objects and mock the http client like you see in the following code.
Retriever is the class we want to test where we inject the AFHTTPClient.
Note that I am passing directly the user and event id, since I want to keep things simple and easy to test. Then in other place you would pass the accout uid value to this method and so on...
The header file would look similar to this:
#import <Foundation/Foundation.h>
#class AFHTTPClient;
#protocol RetrieverDelegate;
#interface Retriever : NSObject
- (id)initWithHTTPClient:(AFHTTPClient *)httpClient;
#property (readonly, strong, nonatomic) AFHTTPClient *httpClient;
#property (weak, nonatomic) id<RetrieverDelegate> delegate;
- (void) retrieveEventWithUserId:(NSString *)userId eventId:(NSString *)eventId;
#end
#protocol RetrieverDelegate <NSObject>
- (void) retriever:(Retriever *)retriever didFindEvenData:(NSDictionary *)eventData;
#end
Implementation file:
#import "Retriever.h"
#import <AFNetworking/AFNetworking.h>
#implementation Retriever
- (id)initWithHTTPClient:(AFHTTPClient *)httpClient
{
NSParameterAssert(httpClient != nil);
self = [super init];
if (self)
{
_httpClient = httpClient;
}
return self;
}
- (void)retrieveEventWithUserId:(NSString *)userId eventId:(NSString *)eventId
{
NSString *path = [NSString stringWithFormat:#"/user/%#/event/%#", userId, eventId];
[_httpClient getPath:path
parameters:nil
success:^(AFHTTPRequestOperation *operation, id responseObject)
{
NSDictionary *eventData = [NSJSONSerialization JSONObjectWithData:responseObject options:0 error:NULL];
if (eventData != nil)
{
[self.delegate retriever:self didFindEventData:eventData];
}
}
failure:nil];
}
#end
And the test:
#import <XCTest/XCTest.h>
#import "Retriever.h"
// Collaborators
#import <AFNetworking/AFNetworking.h>
// Test support
#import <OCMock/OCMock.h>
#interface RetrieverTests : XCTestCase
#end
#implementation RetrieverTests
- (void)setUp
{
[super setUp];
// Put setup code here; it will be run once, before the first test case.
}
- (void)tearDown
{
// Put teardown code here; it will be run once, after the last test case.
[super tearDown];
}
- (void) test__retrieveEventWithUserIdEventId__when_the_request_and_the_JSON_parsing_succeed__it_calls_didFindEventData
{
// Creating the mocks and the retriever can be placed in the setUp method.
id mockHTTPClient = [OCMockObject mockForClass:[AFHTTPClient class]];
Retriever *retriever = [[Retriever alloc] initWithHTTPClient:mockHTTPClient];
id mockDelegate = [OCMockObject mockForProtocol:#protocol(RetrieverDelegate)];
retriever.delegate = mockDelegate;
[[mockHTTPClient expect] getPath:#"/user/testUserId/event/testEventId"
parameters:nil
success:[OCMArg checkWithBlock:^BOOL(void (^successBlock)(AFHTTPRequestOperation *, id))
{
// Here we capture the success block and execute it with a stubbed response.
NSString *jsonString = #"{\"some valid JSON\": \"some value\"}";
NSData *responseObject = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
[[mockDelegate expect] retriever:retriever didFindEventData:#{#"some valid JSON": #"some value"}];
successBlock(nil, responseObject);
[mockDelegate verify];
return YES;
}]
failure:OCMOCK_ANY];
// Method to test
[retriever retrieveEventWithUserId:#"testUserId" eventId:#"testEventId"];
[mockHTTPClient verify];
}
#end
The last thing to comment is that the AFNetworking 2.0 version is released so consider using it if it covers your requirements.

Resources