Carson Carson - 12 days ago 5
HTTP Question

iOS should NOT put extension in NSURL for NSURLSession in HTTP POST

I am trying to write a log-in function for an iOS app, with window azure (ASP.net) as the server. The log-in function should just be simulating a login with a web browser and is written with NSURLSessionDataTask. I am building the http post header fields but the log-in is not successful (while logging in with web browser works), please help!

the codes for the HTTP post:

NSURLSession sessionLogin = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
[[NSHTTPCookieStorage sharedHTTPCookieStorage]setCookieAcceptPolicy:NSHTTPCookieAcceptPolicyAlways];

NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:self.loginPageURL];
[request setHTTPMethod:@"POST"];
[request setHTTPShouldHandleCookies:YES];

NSString* postBodyString = @"";
postBodyString = [postBodyString addStringAtLast:@"__LASTFOCUS="];
postBodyString = [postBodyString addStringAtLast:lastFocus];
postBodyString = [postBodyString addStringAtLast:@"&__VIEWSTATE="];
postBodyString = [postBodyString addStringAtLast:viewState];
postBodyString = [postBodyString addStringAtLast:@"&__VIEWSTATEGENERATOR="];
postBodyString = [postBodyString addStringAtLast:viewStateGenerator];
postBodyString = [postBodyString addStringAtLast:@"&__EVENTTARGET="];
postBodyString = [postBodyString addStringAtLast:eventTarget];
postBodyString = [postBodyString addStringAtLast:@"&__EVENTARGUMENT="];
postBodyString = [postBodyString addStringAtLast:eventArgument];
postBodyString = [postBodyString addStringAtLast:@"&__EVENTVALIDATION="];
postBodyString = [postBodyString addStringAtLast:eventValidation];
postBodyString = [postBodyString addStringAtLast:@"&TxtUserName="];
postBodyString = [postBodyString addStringAtLast:userName];
postBodyString = [postBodyString addStringAtLast:@"&TxtPassword="];
postBodyString = [postBodyString addStringAtLast:password];
postBodyString = [postBodyString addStringAtLast:@"&BtnLogin="];
postBodyString = [postBodyString addStringAtLast:@"Login"];
request.HTTPBody = [postBodyString dataUsingEncoding:NSUTF8StringEncoding];

[request addValue:@"text/html, application/xhtml+xml, */*" forHTTPHeaderField:@"Accept"];
[request addValue:@"http://mywebsite.azurewebsites.net/Login" forHTTPHeaderField:@"Referer"];
[request addValue:@"en-US" forHTTPHeaderField:@"Accept-Language"];
[request addValue:@"Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko" forHTTPHeaderField:@"User-Agent"];
[request addValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
[request addValue:@"gzip, deflate" forHTTPHeaderField:@"Accept-Encoding"];
[request addValue:@"1" forHTTPHeaderField:@"DNT"];
[request addValue:@"no-cache" forHTTPHeaderField:@"Pragma"];

NSURLSessionDataTask* taskLoginPost = [sessionLogin dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (error) {
[self internalLoginFail];
return;
}
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
if (httpResponse.statusCode == 200) {
if ([self checkIsLoggedIn]) {
[self internalLoginSuccessfully];
}else{
[self internalLoginFail];
}
}else{
[self internalLoginFail];
return;
}
});
}];
[taskLoginPost setTaskDescription:@"loginPost"];


(I checked the HTTP response, which now is the same log-in page, not the welcome page.)
The POST body is checked against the numbers from the HTTP GET. The POST body string is:

__LASTFOCUS=&__VIEWSTATE=tTO1jEyw9uujQetfafcpid2ez1LpCrcDjxNjoc%2FDYdfONzPQmnpKPHg6%2FlovCahp29g8SVlDv3XZE2DlP4oh%2B3DUFykzGYAue57wx4xQaoc%3D&__VIEWSTATEGENERATOR=C2EE9ABB&__EVENTTARGET=&__EVENTARGUMENT=&__EVENTVALIDATION=&TxtUserName=userName&TxtPassword=password&BtnLogin=Login


I also used Fiddler on a Window machine (Win 7 Professional SP1 with internet explorer as the browser) to test what http header a web browser will create.

The Fiddler raw header:

POST http://mywebsite.azurewebsites.net/Login HTTP/1.1
Accept: text/html, application/xhtml+xml, */*
Referer: http://mywebsite.azurewebsites.net/Login
Accept-Language: en-US
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip, deflate
Connection: Keep-Alive
Content-Length: 451
DNT: 1
Host: mywebsite.azurewebsites.net
Pragma: no-cache
Cookie: ai_user=p7U3K|2016-11-21T14:00:39.756Z

__LASTFOCUS=&__VIEWSTATE=1U4M1FGvLdr6NMFUo3gwbA6Wpy8jeIk9wMsJuVU19H8ajFWrmvTJ5NJH0UDxyj5NGnoLvo%2BjD16ZKIyhsfiuDPeJj8%2BR76LHzU11E6dcU2s%3D&__VIEWSTATEGENERATOR=C2EE9ABB&__EVENTTARGET=&__EVENTARGUMENT=&__EVENTVALIDATION=bKj4dEUxyrlZvgd61mqLjXawMxG%2BZbwLIKZ3OmWY5nUPrJbPJGqWdxYKJ5XcIV8M6lRHZFYYEgpaFSE0m26sB51Iy6OX18u%2FNU8AtB%2B1mEzhb66CLQdSaB9Wh7S6ONYGH1H4TN5aqrMdVcZLuM4t28qYPmgK9PU7PsZsDwQN5xM%3D&TxtUserName=userName&TxtPassword=password&BtnLogin=Login


I am aware that the iOS document asked us not to touch field "connection" and "host", so those fields are not set. I am guessing there are other header fields should not be touched or cookies not included, please advise!




Further information for the question:

I obtained the view states and event validation by parsing the response from a HTTP GET for the same login page.

The view states parameters were percentage processed with the following code:

string = [string stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet alphanumericCharacterSet]];


The response for the HTTP GET (200) is:


<html xmlns="http://www.w3.org/1999/xhtml">
<head><title>

</title></head>
<body>
<form method="post" action="./Login" id="form1">
<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="hNYEj6Hwo6IQMJtvd/Cleu1GVE+R1swakoSqxOw/MzD9t9rcFA6FeJLXnJtiN0mps4R63bXdDiay1azmtWOOyw+9GUFtuzPsgGeR7OJr1N0=" />

<input type="hidden" name="__VIEWSTATEGENERATOR" id="__VIEWSTATEGENERATOR" value="C2EE9ABB" />
<input type="hidden" name="__EVENTVALIDATION" id="__EVENTVALIDATION" value="IIaKIcjReI3FRpw+340hMXDTtqp5S3Dm6u2hOu0pzvIGfrTGhmw8GMhTPXHjDjQbyhfDgquuwmyCAhtwIva5ceLJbT2bjcKEv2xSa8aZZBgoqqpyzXyAfo2ltj1vek58JHnpi1y73Tm4bGIcimpnQaHaODGdgtrVqpsxAjaEpEA=" />
<div>
<table>
<tr>
<td colspan="2"><h3>Login</h3></td>
</tr>
<tr>
<td style="width:200px">User Name</td>
<td style="width:200px">
<input name="TxtUserName" type="text" id="TxtUserName" /></td>
</tr>
<tr>
<td style="width:200px">Password</td>
<td style="width:200px">
<input name="TxtPassword" type="password" id="TxtPassword" /></td>
</tr>
<tr>
<td style="width:200px"></td>
<td style="width:200px">
<input type="submit" name="BtnLogin" value="Login" id="BtnLogin" /></td>
</tr>
<tr>
<td colspan="2">
<span id="LblStatus"><font color="Red"></font></span>
</td>
</tr>
</table>
</div>
</form>
</body>
</html>


The corresponding HTTP POST body (similar to the one provided before but with corresponding view state parameters):

__LASTFOCUS=&__VIEWSTATE=hNYEj6Hwo6IQMJtvd%2FCleu1GVE%2BR1swakoSqxOw%2FMzD9t9rcFA6FeJLXnJtiN0mps4R63bXdDiay1azmtWOOyw%2B9GUFtuzPsgGeR7OJr1N0%3D&__VIEWSTATEGENERATOR=C2EE9ABB&__EVENTTARGET=&__EVENTARGUMENT=&__EVENTVALIDATION=IIaKIcjReI3FRpw%2B340hMXDTtqp5S3Dm6u2hOu0pzvIGfrTGhmw8GMhTPXHjDjQbyhfDgquuwmyCAhtwIva5ceLJbT2bjcKEv2xSa8aZZBgoqqpyzXyAfo2ltj1vek58JHnpi1y73Tm4bGIcimpnQaHaODGdgtrVqpsxAjaEpEA%3D&TxtUserName=userName&TxtPassword=password&BtnLogin=Login


The response for this HTTP POST is as followed, which is the same login page with code 200:

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head><title>

</title></head>
<body>
<form method="post" action="./Login" id="form1">
<div class="aspNetHidden">
<input type="hidden" name="__LASTFOCUS" id="__LASTFOCUS" value="" />
<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="7wQhosmVuB91RmJm9CZqtGey6rkGTzwh/ytPaVUOTsf+sxAeoKT3zwOGlrRAhF3H8YbkkfLe/LurWRYVmp7FzGPxPKqD2RIsnaeYMnERyHc=" />
</div>

<script type="text/javascript">
//<![CDATA[
var theForm = document.forms['form1'];
if (!theForm) {
theForm = document.form1;
}
function __doPostBack(eventTarget, eventArgument) {
if (!theForm.onsubmit || (theForm.onsubmit() != false)) {
theForm.__EVENTTARGET.value = eventTarget;
theForm.__EVENTARGUMENT.value = eventArgument;
theForm.submit();
}
}
//]]>
</script>


<script src="/WebResource.axd?d=pynGkmcFUV13He1Qd6_TZNQUUtepuM0sahBVKa4djcXHJNs4IjHfPCRkdu-LUQJuvNtmNaMRh_LgkWrVaBEbeg2&amp;t=636093180385155047" type="text/javascript"> </script>


<script src="/WebResource.axd?d=JoBkLzP19aTuxbWOhHobYgm0nosK7P6rQ0lvYYlP0EItV4UWoFwUdFkkH6_2lw2qRb93mcvXAyCwdGo5anHBlg2&amp;t=636093180385155047" type="text/javascript"></script>
<div class="aspNetHidden">

<input type="hidden" name="__VIEWSTATEGENERATOR" id="__VIEWSTATEGENERATOR" value="C2EE9ABB" />
<input type="hidden" name="__EVENTTARGET" id="__EVENTTARGET" value="" />
<input type="hidden" name="__EVENTARGUMENT" id="__EVENTARGUMENT" value="" />
<input type="hidden" name="__EVENTVALIDATION" id="__EVENTVALIDATION" value="HIvhdxMyttpa9nawg7H4VJUId1YEHo4FabT5ymU/GsO84ybDeq68xGUHGlQ43wRdWpqGDu2Tdpvv5bMkzNi03EgI/okDmhMGnNsaQYbT6ARwglFDZTlcGR1B+Zc/pmK9HVA9J1GCSgqYPhvNHgRFyZb/weM/AQskcKAYCa2TX4E=" />
</div>
<div>
<table>
<tr>
<td colspan="2"><h3>Login</h3></td>
</tr>
<tr>
<td style="width:200px">User Name</td>
<td style="width:200px">
<input name="TxtUserName" type="text" id="TxtUserName" style="width:200px;" /></td>
</tr>
<tr>
<td style="width:200px">Password</td>
<td style="width:200px">
<input name="TxtPassword" type="password" id="TxtPassword" style="width:200px;" /></td>
</tr>
<tr>
<td style="width:200px"></td>
<td style="width:200px">
<input type="submit" name="BtnLogin" value="Login" id="BtnLogin" style="width:200px;" /></td>
</tr>
<tr>
<td colspan="2">
<span id="LblStatus" style="color:Red;"></span>
</td>
</tr>
</table>
</div>


<script type="text/javascript">
//<![CDATA[
WebForm_AutoFocus('TxtUserName');//]]>
</script>
</form>
</body>
</html>





After receiving comments, I created a new project with the following codes:

- (void)viewDidLoad {
[super viewDidLoad];
NSString* urlString = @"http://mywebsite.azurewebsites.net/Login.aspx";
NSURL* url = [NSURL URLWithString:urlString];
NSString* userid = @"userName";
NSString* password = @"password";
[self retrieveLoginFieldsFromURL:url completionHandler:^(NSDictionary *parameters) {
[self loginWithURL:url user:userid password:password parameters:parameters completionHandler:^(BOOL success) {
if (success) {
NSLog(@"success");
} else {
NSLog(@"failure");
}
}];
}];
}
- (void)retrieveLoginFieldsFromURL:(NSURL *)url completionHandler:(void (^)(NSDictionary *parameters))completionHandler {
NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (data == nil || error != nil) {
NSLog(@"%@", error);
return;
}

TFHpple *doc = [[TFHpple alloc] initWithHTMLData:data];

NSArray *hidden = [doc searchWithXPathQuery:@"//input[@type='hidden']"];
NSMutableDictionary *parameters = [NSMutableDictionary dictionary];
for (TFHppleElement *element in hidden) {
NSString *key = element[@"id"];
NSString *value = element[@"value"];
if (key) { parameters[key] = value ?: @""; }
}
completionHandler(parameters);
}];
[task resume];
}

- (void)loginWithURL:(NSURL *)url user:(NSString *)user password:(NSString *)password parameters:(NSDictionary *)parameters completionHandler:(void (^)(BOOL))completionHandler {
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
[request setHTTPMethod:@"POST"];

NSMutableDictionary *fullParameters = [parameters mutableCopy];
fullParameters[@"TxtUserName"] = user;
fullParameters[@"TxtPassword"] = password;
fullParameters[@"BtnLogin"] = @"Login";

[request setHTTPBody:[self httpBodyForParameters:fullParameters]];

NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (data == nil || error != nil) {
NSLog(@"%@", error);
return;
}

BOOL success = ![response.URL.path isEqualToString:@"/Login"];

completionHandler(success);
}];
[task resume];
}
- (NSData *)httpBodyForParameters:(NSDictionary *)parameters {
NSMutableArray *parameterArray = [NSMutableArray array];

[parameters enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) {
NSString *param = [NSString stringWithFormat:@"%@=%@", [self percentEscapeString:key], [self percentEscapeString:obj]];
[parameterArray addObject:param];
}];

NSString *string = [parameterArray componentsJoinedByString:@"&"];

return [string dataUsingEncoding:NSUTF8StringEncoding];
}
- (NSString *)percentEscapeString:(NSString *)string {
NSCharacterSet *allowed = [NSCharacterSet characterSetWithCharactersInString:@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~"];
return [string stringByAddingPercentEncodingWithAllowedCharacters:allowed];
}


The only settings I changed in this new project is to change the following item to true in the plist:
NSAppTransportSecurity>NSAllowsArbitraryLoads

I am now suspecting differences in settings for my application.
Please advise.




Thanks Rob for the numerous advices!

The reason detected for not being able to do a HTTP POST to a website is the extension in the NSURL, i.e. no ".aspx" in my case.

Rob Rob
Answer

There are no special headers needed (other than Content-Type).

Having said that, a few observations:

  1. You need to retrieve the login page with GET request, parse the HTML for the contents of the hidden fields. I'd suggest using TFHpple.

  2. You then need to combine these fields with the userid and password fields, and then submit a POST request.

  3. When you build the request, don't forget to percent-escape the values to make sure that your request is well-formed.

  4. You say that it is "failing". You should edit the question to include the error and URLResponse values.

Anyway, I tried the following and it logged in successfully:

[self retrieveLoginFieldsFromURL:url completionHandler:^(NSDictionary *parameters) {
    [self loginWithURL:url user:userid password:password parameters:parameters completionHandler:^(BOOL success) {
        if (success) {
            NSLog(@"success");
        } else {
            NSLog(@"failure");
        }
    }];
}];

Where

- (void)retrieveLoginFieldsFromURL:(NSURL *)url completionHandler:(void (^)(NSDictionary *parameters))completionHandler {
    NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if (data == nil || error != nil) {
            NSLog(@"%@", error);
            return;
        }

        TFHpple *doc = [[TFHpple alloc] initWithHTMLData:data];

        NSArray *hidden = [doc searchWithXPathQuery:@"//input[@type='hidden']"];
        NSMutableDictionary *parameters = [NSMutableDictionary dictionary];
        for (TFHppleElement *element in hidden) {
            NSString *key = element[@"id"];
            NSString *value = element[@"value"];
            if (key) { parameters[key] = value ?: @""; }
        }
        completionHandler(parameters);
    }];
    [task resume];
}

- (void)loginWithURL:(NSURL *)url user:(NSString *)user password:(NSString *)password parameters:(NSDictionary *)parameters completionHandler:(void (^)(BOOL))completionHandler {
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    [request setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
    [request setHTTPMethod:@"POST"];

    NSMutableDictionary *fullParameters = [parameters mutableCopy];
    fullParameters[@"TxtUserName"] = user;
    fullParameters[@"TxtPassword"] = password;
    fullParameters[@"BtnLogin"] = @"Login";

    [request setHTTPBody:[self httpBodyForParameters:fullParameters]];

    NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if (data == nil || error != nil) {
            NSLog(@"%@", error);
            return;
        }

        // we can determine success from whether we're still at login page or not; if so, we failed; if not, we succeeded

        BOOL success = ![response.URL.path isEqualToString:@"/Login"];

        completionHandler(success);
    }];
    [task resume];
}

/** Build the body of a `application/x-www-form-urlencoded` request from a dictionary of keys and string values

 @param parameters The dictionary of parameters.
 @return The `application/x-www-form-urlencoded` body of the form `key1=value1&key2=value2`
 */
- (NSData *)httpBodyForParameters:(NSDictionary *)parameters {
    NSMutableArray *parameterArray = [NSMutableArray array];

    [parameters enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) {
        NSString *param = [NSString stringWithFormat:@"%@=%@", [self percentEscapeString:key], [self percentEscapeString:obj]];
        [parameterArray addObject:param];
    }];

    NSString *string = [parameterArray componentsJoinedByString:@"&"];

    return [string dataUsingEncoding:NSUTF8StringEncoding];
}

/** Percent escapes values to be added to a URL query as specified in RFC 3986.

 See http://www.ietf.org/rfc/rfc3986.txt

 @param string The string to be escaped.
 @return The escaped string.
 */
- (NSString *)percentEscapeString:(NSString *)string {
    NSCharacterSet *allowed = [NSCharacterSet characterSetWithCharactersInString:@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~"];
    return [string stringByAddingPercentEncodingWithAllowedCharacters:allowed];
}
Comments