4

I'm trying to make a post to an HTTP server

By the way this is my code:

  func sendPostToUrl(url:String, withParams params: [String: String?] ) {
    var request = NSMutableURLRequest(URL: NSURL(string: url)!)
    var session = NSURLSession.sharedSession()
    request.HTTPMethod = "POST"

    var err: NSError?
    var bodyData = ""
    for (key,value) in params{
        if (value==nil){ continue }
        let scapedKey = key.stringByAddingPercentEncodingWithAllowedCharacters(
                 .URLHostAllowedCharacterSet())!
        let scapedValue = value!.stringByAddingPercentEncodingWithAllowedCharacters(
                .URLHostAllowedCharacterSet())!
        bodyData += "\(scapedKey)=\(scapedValue)&"
    }
    request.HTTPBody = bodyData.dataUsingEncoding
               (NSUTF8StringEncoding, allowLossyConversion: true)

    var task = session.dataTaskWithRequest(request, 
    completionHandler: {data, response, error -> Void in
        println("Response: \(response)")
        let dataString = NSString(data: data, encoding: NSUTF8StringEncoding)
        println("Data: \(dataString)")

    })
    task.resume()
}

It works but is not perfect. If I call the function this way:

    client.sendPostToUrl("http://novagecko.com/tests/test.php", 
        withParams: ["hello":"world","inject":"param1=value1&param2=value2"]);

The server detects 3 post fields (with keys hello,inject and param2) instead of 2.

How can I escape the key and values?

Is there something more I could do for improving the method?

jscs
  • 63,694
  • 13
  • 151
  • 195
Addev
  • 31,819
  • 51
  • 183
  • 302

2 Answers2

10

If you can target iOS 8 (thanks @Rob), use NSURLComponents to escape your parameters instead:

import Foundation

func encodeParameters(#params: [String: String]) -> String {
    var queryItems = map(params) { NSURLQueryItem(name:$0, value:$1)}
    var components = NSURLComponents()
    components.queryItems = queryItems
    return components.percentEncodedQuery ?? ""
}

Now encodeParameters(params:["hello":"world","inject":"param1=value1&param2=value2"]) returns hello=world&inject=param1%3Dvalue1%26param2%3Dvalue2 as you would expect.

Otherwise, the best way to create the character set that will let you escape your values properly is this:

var safeCharacterSet = NSCharacterSet.URLQueryAllowedCharacterSet().mutableCopy()
safeCharacterSet.removeCharactersInString("&=")

and see @rintaro's answer to use filter/map properly to perform the encoding in a nice way.

Thomas Deniau
  • 2,488
  • 1
  • 15
  • 15
  • I agree that your character set seems to replicate what `percentEncodedQuery` does, but there's a problem: That character set is letting `+` pass unescaped! Web servers replace `+` with space (as dictated by `x-www-form-urlencoded` spec). The `+` should be added to your characters to remove from the safe character set. I think that the `percentEncodedQuery` failure to escape `+` is worthy of a bug report. – Rob Nov 27 '14 at 15:07
  • Good catch. I replicated what `percentEncodedQuery` does indeed. – Thomas Deniau Nov 27 '14 at 16:00
  • I think the issue is that NSURLComponents follows RFC3986 which doesn't say anything about the format of the query string. The docs don't mention x-www-form-urlencoded at all, so the result might be expected... Not sure it's bug-worthy. If you do file a bug please paste the number here! – Thomas Deniau Nov 27 '14 at 16:12
  • 1
    I respectfully disagree that their implementation accurately follows RFC 3986, which includes `+` in the "reserved" characters to be percent encoded unless "these characters are specifically allowed by the URI scheme to represent data in that component" (section 2.2), which `+` definitely is not, in query data at least. That RFC defines rintaro's character set as the only characters that should never be percent escaped (section 2.3). This is all academic: The `+` definitely needs to be percent escaped or else the server will replace with space. I'll file a bug report. – Rob Nov 27 '14 at 16:55
  • This is definitely pedantic, but the RFC says that the characters which do not have to be encoded in the query part are pchar / "/" / "?" where pchar is defined as unreserved / pct-encoded / sub-delims / ":" / "@", and subdelims includes + (and &, and =). Clearly &s and =s are encoded in the key/values because otherwise everything would break, but I'd argue you need an additional spec (maybe x-www-form-urlencoded) to make you encode + – Thomas Deniau Nov 27 '14 at 18:00
  • I agree that you need another spec to define the `+` behavior. But you need that for `&` (and even `=`), too. This RFC doesn't attempt to define details of any query `sub-delim` behaviors. It only says, in short, that (a) do not escape `sub-delim` when using it as such; (b) do escape it when using it in data and it might be confused for its role as `sub-delim`; and (c) the only characters that you never escape are the unreserved characters (rintaro's list). – Rob Nov 28 '14 at 04:14
  • this solution also does not escape ? which is pretty lethal. I filed radar://24050534 about it. Other potentially-dangerous unescaped characters: !$'()*+,/:;?@ – OneSadCookie Jan 05 '16 at 01:54
2

It seems, NSCharacterSet doesn't have relevant set for that.

So, add this

extension NSCharacterSet {
    class func URLUnreservedCharacterSet() -> NSCharacterSet {
        return self(charactersInString: "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_.~")
    }
}

Then

var bodyData = ""
var safeCharacterSet = NSCharacterSet.URLUnreservedCharacterSet()
for (key,value) in params{
    if (value==nil){ continue }
    let scapedKey = key.stringByAddingPercentEncodingWithAllowedCharacters(safeCharacterSet)!
    let scapedValue = value!.stringByAddingPercentEncodingWithAllowedCharacters(safeCharacterSet)!
    bodyData += "\(scapedKey)=\(scapedValue)&"
}

As following @Rob's advice in comment, here is a map and join example:

let params:[String:String?] = ["fubar":nil, "hello":"world", "inject":"param1=value1&param2=value2"]

let safeCharacterSet = NSCharacterSet.URLUnreservedCharacterSet()
let pairs = filter(params, {$1 != nil}).map { (key, value) -> String in
    let _key = key.stringByAddingPercentEncodingWithAllowedCharacters(safeCharacterSet)!
    let _val = value!.stringByAddingPercentEncodingWithAllowedCharacters(safeCharacterSet)!
    return _key + "=" + _val
}
let bodyData = "&".join(pairs)

This is better because there is no trailing & in the result.

rintaro
  • 51,423
  • 14
  • 131
  • 139
  • Instead of hardcoding the character set like this (you're most probably missing characters...) you can do this: `var safeCharacterSet = NSCharacterSet.URLQueryAllowedCharacterSet().mutableCopy(); safeCharacterSet.removeCharactersInString("&=")` – Thomas Deniau Nov 27 '14 at 09:09
  • FWIW, rintaro's character set conforms to RFC 3986's definition of "unreserved" characters and is preferable, IMHO. See http://stackoverflow.com/a/24888789/1271826 for a few references. At the very least, if you're going to use this `removeCharactersInString` approach, include the `+` character, too. – Rob Nov 27 '14 at 15:10