Best way to parse SSDP Discovery in Objective C - objective-c

I am working on a very simple application to discover a device using SSDP and I am trying to find the easiest way to parse the response from this command. I am trying to avoid having to do a bunch of NSString or regular expressions manipulations.
I have tried the following two approaches:
Approach 1:
Using GCDAsyncUdpSocket, I am able to successfully send the discovery command and get the following response:
HTTP/1.1 200 OK
Cache-Control: max-age=300
ST: roku:ecp
USN: uuid:roku:ecp:1234567890
Ext:
Server: Roku UPnP/1.0 MiniUPnPd/1.4
Location: http://192.168.XX.XX:8060/
This looks like a regular HTTP response, but using GCDAsyncUdpSocket, I am getting the response as an NSData object, which I can easily convert to an NSString. However, what would be ideal is to somehow cast this to an NSHTTPURLResponse and then use its methods to get the field values. Any idea if this can be done?
Approach 2:
I have tried using a regular NSURLRequest to try to send this command and then I would be able to get an NSHTTPURLResponse back. However, I keep on getting an error because the SSDP discovery command requires me to send this request to port 1900.
I use the following code to send the "HTTP" request, I know it is not strictly HTTP, but I thought it may be an easier way to send this UDP command as the requirements look very similar to HTTP.
NSURL *url = [NSURL URLWithString:#"http://239.255.255.250:1900"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url
cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:60.0];
[request setHTTPMethod:#"M-SEARCH *"];
[request setValue:#"239.255.255.250:1900" forHTTPHeaderField:#"Host"];
[request setValue:#"\"ssdp:discover\"" forHTTPHeaderField:#"Man"];
[request setValue:#"roku:ecp" forHTTPHeaderField:#"ST"];
NSURLConnection *connection = [[NSURLConnection alloc]initWithRequest:request delegate:self startImmediately:YES];
if (connection)
{
NSLog(#"Connection success!");
}
else
{
NSLog(#"Connection failed!");
}
When I do this, the connection is successful, but I get the following error in the didFailWithError delegate for NSURLConnection:
failed Error Domain=NSPOSIXErrorDomain Code=47 "The operation couldn’t be completed. Address family not supported by protocol family" UserInfo=0x8875940 {NSErrorFailingURLKey=http://239.255.255.250:1900/, NSErrorFailingURLStringKey=http://239.255.255.250:1900/}
This error only happens if I use port 1900, if I leave this out or use another more HTTP friendly port such as 8080, then this works, but obviously the device I am trying to discover does not respond correctly unless it gets the request in port 1900.
Thanks for any help you can provide.

Port 1900 is fixed for SSDP. All UPnP devices are fixed to listen on this port and the connecting nodes expect that. You can't change it. It's a part of the UPnP design goal to "work out of the box". Furthermore, my Cocoa expertise is very limited, but i think that NSHTTPURLResponse won't make things simpler for you. [NSHTTPURLResponse allHeaderFields] returns NSDictionary, which means that you don't know anymore what was the original order of the header fields. And you need to know what was coming after Ext: which is a meta-header.
I suggest either parsing the response yourself, which shouldn't be much more complicated than one cycle, separating the response by lines and then splitting by :. Or instead of trying to roll your own SSDP handshake, use ready made Cocoish library like Upnpx, Cyberlink or Platinum. It might feel like inappropriately heavy artillery just for the discovery phase, but i wonder what else you would do with the device after that, other than actually trying to invoke some actions on the device.

Related

What is the correct way of sending large file through HTTP POST, without loading the whole file into ram?

I'm currently working on an application for uploading large video files from the iPhone to a webservice through simple http post. As of right now, I build an NSURLRequest and preload all of the video file data before loading the request. This naturally eats a ton of ram if the file is considerably big, in some cases it's not even possible.
So basically my question is: Is there a correct way of streaming the data or loading it in chunks without applying any modifications to the webserver?
Thanks.
EDIT for clarification: I am searching for a way to stream large multipart/form data FROM the iPhone TO a webserver. Not the other way arround.
EDIT after accepting answer: I just found out that apple has some nifty source code written for this exact purpose and it shows appending additional data to the post not just the big file itself. Incase anyone ever needs it: SimpleURLConnections - PostController.m
Yet another EDIT: While using that piece of source code from apple I encountered a very stupid and ugly problem that even wireshark couldn't help me debug. Some webservers don't understand the boundary string when it's declared in between quotes (like in apples example). I had problems with it on Apache Tomcat and removing the quotes worked just wonderful.
You can use NSInputStream on NSMutableURLRequest. For example:
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:uploadURL];
NSInputStream *stream = [[NSInputStream alloc] initWithFileAtPath:filePath];
[request setHTTPBodyStream:stream];
[request setHTTPMethod:#"POST"];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
NSLog(#"Finished with status code: %i", [(NSHTTPURLResponse *)response statusCode]);
}];
You can use a NSInputStream to provide the data to post via -[NSMutableURLRequest setHTTPBodyStream:]. This could be an input stream that reads from a file. You might need to implement the connection:needNewBodyStream: method in your URL connection delegate to provide a new, unopened stream in case the system needs to retransmit the data.
One way to do this is to use an asynchronous NSInputStream in concert with a file. When the asynchronous connection asks you to provide more data, you read in the data from a file. You have a few ways to do this:
UNIX/BSD interface. use open (or fopen), malloc, read, and create a NSData object from the malloced data
use the above with mmap() if you know it
use the Foundation class NSFileHandle APIs to do more or less the same using ObjectiveC
You can read up on streams in the 'Stream Programming Guide'. If this doesn't work for you there are lots of open source projects that can upload files, for instance MKNetworkKit

AFNetworking HTTP PUT Request

I have the following API call:
URL: /api/some-call
Method: PUT
PARAMS: No params
Its just a simple PUT method. I am trying to use AFNetworking to do that and unfortunately, I am failing. Here's what I have right now:
AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:url];
NSMutableURLRequest *req = [httpClient requestWithMethod:#"PUT" path:#"" parameters:nil];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:req];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"Success");
} failure: ^(AFHTTPRequestOperation *operatn, NSError *error) {
NSLog(#"Failure");
}];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:operation];
This is however, not working. Why is that? Furthermore, what is path supposed to be in a PUT request? I've tried several things and this is what I have now at the end, which I believe should be close to what is correct.
One last question: AFNetworking does not use ARC. Does that mean I still need the autorelease at the end of the NSOperationQueue statement?
EDIT:
Here is error NSLog: Failure Error Domain=com.alamofire.networking.error Code=-1011 "Expected status code in (200-299), got 409" UserInfo=0x7a91fb0 {NSErrorFailingURLKey=*the url*/api/some-call, NSLocalizedDescription=Expected status code in (200-299), got 409}
Well. You are getting a 409 error code.
Quoted from http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html :
10.4.10 409 Conflict
The request could not be completed due to a conflict with the current
state of the resource. This code is only allowed in situations where
it is expected that the user might be able to resolve the conflict and
resubmit the request. The response body SHOULD include enough
information for the user to recognize the source of the conflict.
Ideally, the response entity would include enough information for the
user or user agent to fix the problem; however, that might not be
possible and is not required.
Conflicts are most likely to occur in response to a PUT request. For
example, if versioning were being used and the entity being PUT
included changes to a resource which conflict with those made by an
earlier (third-party) request, the server might use the 409 response
to indicate that it can't complete the request. In this case, the
response entity would likely contain a list of the differences between
the two versions in a format defined by the response Content-Type.
Which means the error is caused by your server not with your code. unless you have provided some wrong parameters.
Well. As for the question regarding the "what is the path supposed to be in PUT".
Normally I'll put baseURL as the domain name of the server.
Which is something like
http://localhost
then i'll put the path to be something like
#"the/rest/of/the/api/url"
then it's easier to switch between development and production servers with just a switch of a baseURL. :)
And for your last question, "AFNetworking does not use ARC. Does that mean I still need the autorelease at the end of the NSOperationQueue statement?"
Does that mean your project is using ARC with AFnetworking, or AFNetworking WITHOUT ARC.
if it's ARC with AFNetworking, you don't have to. Take a look at this
https://github.com/AFNetworking/AFNetworking#arc-support
if it's non-ARC with AFNetworking, you basically have to do all the memory management yourself. :)
Hit me up again if you need more info and i'll edit accordingly. :)
Hope i've helped in someway.

Webserver NSURL NSURLConnection

Ok, I'm trying to get a file from my webserver. But I'm getting kinda confused about some stuff. When I use NSURL I can get my xml-file with an url like this: "localhost...../bla.xml". But I'm also trying to test some things... Like... What will happen to my app if I have an open connection to the webserver, and I lose connection to internet? The above method with the NSURL, I haven't really established any connection where it always is connected right? or should I use be using NSURLConnection?
Maybe it's a little confusing, because I'm confused. I hope someone can give me some info I can research about.
Thanks.
Take a look at NSURLConnection Class.
http://developer.apple.com/library/mac/#documentation/Cocoa/Reference/Foundation/Classes/NSURLConnection_Class/Reference/Reference.html
Create connection object and set a timeout value, if you lose the connection or the connection times out NSURLConnection delegate method: - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error gets called and you would be notified of that event.
You might also use NSURLConnection sendSynchronousRequest method, but its strongly discouraged to use that method as it would block the thread its running.
Are you trying to access the content of a file? If so, you would use the following.
NSError *error;
NSURL *url = [NSURL URLwithString:#"localhost/file.html"];
NSString *filecontents = [NSString stringWithContentsOfURL:url encoding:NSUTF8StringEncoding error:&error];
The NSString object filecontents would contain your string.
You wouldn't be able to loose connection in such a short time. If there is no connection, an error would be applied to error.
EDIT:
If you wanted to constantly stay connected to a server, that is a different story.
You have have to use C's send() and recv() functions, which you can read about here.
I don't know much about it, and I'm learning it myself, so you should ask someone else on how to set up a server. But you will need to have another program running simultaniously.

Cocoa HTTP PUT with content-range

Is it possible to use an NSURLConnection/NSURLRequest combination to send a PUT request to a server with a Content-Range header? By that I mean I want to resume an upload to the server which can accept a byte range in the request to resume the upload.
I see you can set an NSInputStream as the request body so I figured that I could subclass that and override the the open/seek functions and set the request header but it seems to call on undocumented selectors and break the implementation.
I'm sure I could do this with CFNetwork but it just seems like there must be a way to do it with the higher level APIs.
Any ideas on where to start?
EDIT:
To answer my own question, this is indeed possible after reading a blog [http://bjhomer.blogspot.com/2011/04/subclassing-nsinputstream.html] which details the undocumented callbacks which relate to CFStream. Once those are implemented I can call the following in the open callback to skip ahead:
CFReadStreamSetProperty((CFReadStreamRef)parentStream, kCFStreamPropertyFileCurrentOffset, (CFNumberRef)[NSNumber numberWithUnsignedLongLong:streamOffset]);
Thanks,
J
I think the server needs to supports put method combines with range but this will be the way to do it with high level Objective-C API
NSURL *URL = [NSURL URLWithString:strURL];
NSMutableURLRequest *urlRequest = [NSMutableURLRequest requestWithURL:URL];
NSString *range = [NSString stringWithFormat:#"bytes=%lld-%lld",bytesUploaded,uploadSize];
[urlRequest addValue:range forHTTPHeaderField:#"Range"];
[urlRequest setHTTPMethod:#"PUT"];
self.connection = [NSURLConnection connectionWithRequest:urlRequest delegate:self];
Cheers
First, if you want to do fancy work with HTTP, I typically recommend ASIHTTPRequest. It's solid stuff that simplifies a lot of more complicated HTTP problems. It's not really needed for setting a simple header, but if you're starting to build something more complex, it can be nice to move over to ASI sooner rather than later.
With an NSMutableURLRequest, you can set any header you want using addValue:forHTTPHeaderField:. You can use that to set your Content-Range.
Like I posted in my comment, you can facilitate what you want without dropping down to the CoreFoundation level:
As NSInputStream inherits NSStream, it is possible to prepare the stream as follows:
NSNumber *streamOffset = [NSNumber numberWithUnsignedInteger:lastOffset];
[inputStream setProperty:streamOffset forKey: NSStreamFileCurrentOffsetKey];
(Assuming lastOffset is an NSUInteger representation of your last file offset in bytes and inputStream is the stream you want to set as the request's HTTPBodyStream.)

ASIFormDataRequest unable to upload .zip file from device but does it in simulator

I have wifi connected in the devices - so this is ruled out.
The Application i have with the same following code executed is able to upload the file to a java server in the iOS 4.3 simulator but unable to upload in iOS 4.3.3 device. This is kind of strange.
ASIFormDataRequest *request_zip = [[ASIFormDataRequest alloc] initWithURL:[NSURL URLWithString:strURL]];
[request_zip setAllowCompressedResponse:YES];
[request_zip setPostValue:#"device" forKey:#"value1"]; //pass device ID here...
//[request_zip addRequestHeader:#"Content-Type" value:#"multipart/form-data"];
[request_zip setTimeOutSeconds:20];
[request_zip setDelegate:self];
[request_zip setDidFailSelector:#selector(uploadFailed:)];
[request_zip setDidFinishSelector:#selector(uploadFinished:)];
[request_zip setFile:path forKey:path];
[request_zip startAsynchronous];
NSLog(#"%# post length",[NSString stringWithFormat:#"%llu",[request_zip postLength]]);
The code when executed results the following output in the terminal.
Incorrect NSStringEncoding value 0x0000 detected. Assuming NSStringEncodingASCII. Will stop this compatiblity mapping behavior in the near future.
The post length printed in the console =>
0 post length
There is also a another string comes up lastly, i.e the time out message,
Request failed:
The request timed out with response data
100% sure that the server is active and responds immediately for the app executed from simulator.
how is it possible to have a program running in simulator properly but not in the device?
Incorrect NSStringEncoding value
0x0000 detected. Assuming
NSStringEncodingASCII. Will stop this
compatiblity mapping behavior in the
near future.
Means that you have a NSString that is being initalized without a NSStringEncoding value, check your NSString calls.
Empty body in POST in ASIHTTPRequest
Try:
NSURL *requestURL = [NSURL URLWithString:[NSString stringWithFormat:#"%#", strURL]];
ASIFormDataRequest *request_zip = [ASIFormDataRequest requestWithURL:requestURL];
The NSLog will fire right after the [request_zip startAsynchronous]; which at that time may just be initialized, you need to move the log request into a delegate method, change this to [request_zip startSynchronous]; and it will fire immediately,.
Then your delegate method will look like this:
- (void)requestStarted:(ASIHTTPRequest *)request {
NSLog(#"%# post length",[NSString stringWithFormat:#"%llu",[request postLength]]);
}
The problem was with the server end. ASIHTTP lib works properly, when ASI tries to transmit the request with post body to server, the apache didnt respond back properly as one of the server configuration was missing probably. The proprietary web library owners fixed it later and the problem is resolved.
It worked in simulator but not in device, why?
It worked in simulator coz the data is transmitted thru the ethernet not wireless, so the transmission rates were great comparatively.
Web service developers has to plz make sure that the data is received even
if it comes in from low speed networks
**
ASI libs returned ASCII encoding error, why?
**
This error is returned by ASI immediately once the request goes time - out. This is the cause, but the real internal problem with ASI for this error has to be found out.
Very interesting results fetched end of the day.