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:
- When reading a response, get its X-Transmission-Session-Id header and remember it
- Add the updated header to your outgoing requests
- 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.log
s), 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?
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