Remote Transmission session doesn't respond after providing corrent session id

Well, that would be a rather obscure topic but I'll give it a try, maybe someone will know the answer.

I am writing a little remote Node.js client for the Transmission BitTorrent Client. Communication is handled via RPC using JSON objects.

Here is the specification.

And my code (written in CoffeeScript, if that is a problem I can provide the equivalent JavaScript code also, just didn't want to make this question too long to read), the important part:

runRemoteCommand: (params) ->
  # convert JSON object to string
  params = JSON.stringify params #, null, "  "

  # set request options
  options = 
    host: @config.host
    port: @config.port
    path: @config.rpcPath
    auth: "#{@config.username}:#{@config.password}"
    headers:
      'Content-Type': 'application/json'
      'Content-Length': params.length
    method: 'GET'

  # we don't know the session id yet
  sessionId = false

  # wrapped in a function so it could be run two times, not really a finished solution
  run = () =>
    # If the session id is provided, set the header, as in 2.3.1 of the specs
    if sessionId
      options.headers["X-Transmission-Session-Id"] = sessionId

    # define the request object and a callback for the response
    request = @http.get options, (response) =>
      # log everything for debug purposes
      console.log "STATUS: #{response.statusCode}"
      console.log "HEADERS: #{JSON.stringify response.headers}"
      response.setEncoding 'utf8'
      response.on "data", (data) =>
        console.log "BODY: #{data}"

      # if status code is 409, use provided session id
      if response.statusCode == 409
        sessionId = response.headers["x-transmission-session-id"]
        console.log "sessionId: #{sessionId}"
        # running it immediately sometimes caused the remote server to provide a 501 error, so I gave it a timeout
        setTimeout run, 5000

    # no output here
    request.on "error", (e) =>
      console.log "ERROR: #{e}"

    # actually send the request
    request.write params
    request.end()

  # run our function
  run()

The params variable is defined as:

params =
  "arguments":
    "filename": link
  "method": "torrent-add"
  "tag": 6667

Everything works fine until I set a valid session id. On the first time the run function is called, I get the following output (formatted it a little to be more eye-friendly):

STATUS: 409

HEADERS:

{
  "server":"Transmission",
  "x-transmission-session-id":"io4dOLm8Q33aSCEULW0iv74SeewJ3w1tP21L7qkdS4QktIkR",
  "date":"Wed, 04 Apr 2012 08:37:37 GMT",
  "content-length":"580",
  "content-type":"text/html; charset=ISO-8859-1"
}

sessionId: io4dOLm8Q33aSCEULW0iv74SeewJ3w1tP21L7qkdS4QktIkR

BODY:

409: Conflict

Your request had an invalid session-id header.

To fix this, follow these steps:

  1. When reading a response, get its X-Transmission-Session-Id header and remember it
  2. Add the updated header to your outgoing requests
  3. When you get this 409 error message, resend your request with the updated header

This requirement has been added to help prevent CSRF attacks.

X-Transmission-Session-Id: io4dOLm8Q33aSCEULW0iv74SeewJ3w1tP21L7qkdS4QktIkR

Which is exactly what should be returned by the remote server when no session id is provided. However, after setting the session id in the header, the server doesn't respond. The second run call is fired and the request is sent (confirmed by placing some useful console.logs), but the response callback is never fired. I receive no response from the remote server and my application freezes waiting.

I'm pretty sure the error is on my side, not on the server's, because an out-of-the-box remote client for android works just fine when connecting to the same remote session.

Am I performing the request correctly? Especially the JSON part?

EDIT: A little test

I have written a little php script to test if the JSON-encoded request is ok and used it as a "fake" remote transmission. Here it is:

$headers = apache_request_headers();

// Simulate transmission's behavior
if (!isset($headers['X-Transmission-Session-Id'])) {
    header("HTTP/1.0 409 Conflict");
    header("X-Transmission-Session-Id: test");
}

print_r($headers);

// Is there a nicer way to get the raw request?
print_r(file_get_contents('php://input'));

And, personally, I don't see anything wrong in the data outputted by this test. After returning the 409 status code, the Node.js app properly assigns the session id for the request. The first print_r prints an array:

Array
(
    [Content-type] => application/json
    [Content-length] => 152
    [X-Transmission-Session-Id] => test
    [Host] => tp.localhost
    [Connection] => keep-alive
)

The second one prints a string, which is a properly formatted JSON string (nothing more in it):

{
  "arguments": {
    "filename": "http://link-to-torrent"
  },
  "method": "torrent-add",
  "tag": 6667
}

I really can't see what am I doing wrong. Some third-party clients which I tested with the same remote server work properly.

Havng the same issue i've done this class. I'm thinking better way do a getData, method. But it works.

http = require "http"
_ = require "underscore"

class Connect
  constructor: (@login, @password, @host='127.0.0.1', @port=9091, @headers={}) ->

  getData: (params)->
    key = "x-transmission-session-id"

    options = {
      host: @host
      port: @port
      path: '/transmission/rpc',
      method: 'POST',
      headers: @headers || {},
      auth: "#{ @login }:#{ @password }"  
    }

    _.extend options, params || {}

    req = http.request(options, (res)=>
      if res.statusCode == 401
        console.log "Auth errror"

      else if res.statusCode == 409
        auth_header={}
        auth_header[key] = res.headers[key]

        _.extend @headers, auth_header
        @getData(params)

      else if res.statusCode == 200
        res.setEncoding 'utf8'
        res.on('data', (chunk)->
          #here should be an emmit of data
          console.log chunk
        )

      else
        console.log "Error #{ res.statusCode }"

    )
    req.write('data\n')
    req.write('data\n')
    req.end()

connector = new Connect "transmission", "password"

connector.getData()

Well, I was able to circumvent - but not solve - the problem, using mikeal's request, which also simplified my code. The most recent version looks like this:

runRemoteCommand: (params, callback = false) =>
  options =
    uri: @uri
    method: "POST"
    json: params

  if @sessionId
    options.headers = 
      "X-Transmission-Session-Id": @sessionId

  request options, (error, response, body) =>
    retVal = 
      success: false
    end = true

    if error
      retVal.message = "An error occured: #{error}"
    else
      switch response.statusCode
        when 409
          if response.headers["x-transmission-session-id"]
            @sessionId = response.headers["x-transmission-session-id"]
            end = false
            @.runRemoteCommand params, callback
          else
            retVal.message = "Session id not present"
        when 200
          retVal.success = true
          retVal.response = body
        else retVal.message = "Error, code: #{response.statusCode}"

     callback retVal if end && callback

I'll leave this answer unaccepted for the time being because I still don't know what was wrong with the "raw" version.

#!/bin/bash

#-----------------------------------------------------------------------
#

DEBUG=0
HOST="192.168.1.65"
PORT="8181"
TRURL="http://$HOST:$PORT/transmission/rpc"
USER="admin"
PASSWORD="password1"
XTSID=""


#-------------------------------------
# 
function getSID ()
{
    local S="$1"
    S=${S##*X-Transmission-Session-Id: }
    S=${S%%</code>*}
    echo $S
}

#-------------------------------------
function getData ()
{
    local REQUEST="$1"

    local RET=$(curl --silent -H "X-Transmission-Session-Id: $XTSID" \
        -H "Content-type: application/json" \
        -X POST \
        -d "$REQUEST" \
        --user $USER:$PASSWORD $TRURL)

    ((DEBUG)) && echo $XTSID

    if [[ "$RET" =~ "409: Conflict" ]] 
    then
        XTSID=$(getSID "$RET")

        ((DEBUG)) && echo "XTSID $XTSID"

        RET=$(curl --silent -H "X-Transmission-Session-Id: $XTSID" \
            -H "Content-type: application/json" \
            -X POST \
            -d "$REQUEST" \
            --user $USER:$PASSWORD $TRURL)
    fi

    echo $RET
}

#-------------------------------------

R='{"method":"session-stats"}'
RET=$(getData "$R")
echo $RET