/// <summary> /// Waits for a client connection to be made and subsequently verifies it. /// </summary> /// <param name="session">The SI session whose data to transfer.</param> /// <param name="server">The instance of the SOCKS5 server to accept /// client connections on.</param> /// <param name="timeout">The number of milliseconds to wait for a client /// connection before returning to the caller, or -1 to wait /// indefinitely.</param> /// <exception cref="SocketException">An error occurred when accessing the /// underlying socket of the SOCKS5 server instance.</exception> /// <exception cref="Socks5Exception">The SOCKS5 negotiation with the client /// failed.</exception> /// <exception cref="TimeoutException">A timeout was specified and it /// expired.</exception> /// <exception cref="IOException">The stream could not be read, or the /// operation timed out.</exception> void AcceptClientConnection(SISession session, Socks5Server server, int timeout = -1) { var request = server.Accept(timeout); if (request.Command != SocksCommand.Connect) { throw new Socks5Exception("Unexpected SOCKS5 command: " + request.Command); } if (request.ATyp != ATyp.Domain) { throw new Socks5Exception("Unexpected ATyp: " + request.ATyp); } string hash = (string)request.Destination; // Calculate the SHA-1 hash and compare it with the one in the request. string calculated = Sha1(session.Sid + im.Jid + session.To); if (calculated != hash) { throw new Socks5Exception("Hostname hash mismatch."); } // We're good to go. server.Reply(ReplyStatus.Succeeded, hash, request.Port); }
/// <summary> /// Processes an IBB 'data' request. /// </summary> /// <param name="sessionId">The mandatory session id attribute of the 'data' /// element.</param> /// <param name="stanza">The IQ stanza containing the request.</param> /// <exception cref="ArgumentNullException">The sessionId parameter or the /// stanza parameter is null.</exception> /// <exception cref="ArgumentException">The IQ stanza is missing the /// mandatory 'data' element, or the specified session id is not associated /// with a file-stream.</exception> /// <exception cref="FormatException">The data contained in the 'data' element /// is not a valid BASE64-encoded string.</exception> /// <exception cref="IOException">The data could not be written to the /// file-stream. Consult the InnerException property of the IOException object /// to obtain the specific reason.</exception> void Data(string sessionId, Iq stanza) { sessionId.ThrowIfNull("sessionId"); stanza.ThrowIfNull("stanza"); var data = stanza.Data["data"]; if (data == null) { throw new ArgumentException("Invalid stanza, missing data element."); } SISession session = siFileTransfer.GetSession(sessionId, stanza.From, im.Jid); if (session == null) { throw new ArgumentException("Invalid session-id."); } string base64 = data.InnerText; // Decode base64 string and write decoded binary data to file. byte[] bytes = Convert.FromBase64String(base64); try { session.Stream.Write(bytes, 0, bytes.Length); } catch (Exception e) { throw new IOException("The stream could not be written.", e); } // Update the byte count and raise the 'BytesTransferred' event. session.Count = session.Count + bytes.Length; BytesTransferred.Raise(this, new BytesTransferredEventArgs(session)); }
/// <summary> /// Performs a mediated transfer, meaning we send the data over a proxy server. /// </summary> /// <param name="session">The SI session whose data to transfer.</param> /// <param name="proxies"></param> /// <exception cref="Socks5Exception">The SOCKS5 connection to the designated /// proxy server could not be established.</exception> /// <exception cref="XmppErrorException">The server returned an XMPP error code. /// Use the Error property of the XmppErrorException to obtain the specific /// error condition.</exception> /// <exception cref="XmppException">The server returned invalid data or another /// unspecified XMPP error occurred.</exception> void MediatedTransfer(SISession session, IEnumerable <Streamhost> proxies) { var proxy = NegotiateProxy(session, proxies); // Connect to the designated proxy. using (var client = new Socks5Client(proxy.Host, proxy.Port)) { // Send the SOCKS5 Connect command. string hostname = Sha1(session.Sid + session.From + session.To); SocksReply reply = client.Request(SocksCommand.Connect, hostname, 0); if (reply.Status != ReplyStatus.Succeeded) { throw new Socks5Exception("SOCKS5 Connect request failed."); } // Activate the bytetream. var xml = Xml.Element("query", "http://jabber.org/protocol/bytestreams") .Attr("sid", session.Sid).Child( Xml.Element("activate").Text(session.To.ToString())); Iq iq = im.IqRequest(IqType.Set, proxy.Jid, im.Jid, xml); if (iq.Type == IqType.Error) { throw Util.ExceptionFromError(iq, "Could not activate the bytestream."); } // Finally, go ahead and send the data to the proxy. SendData(session, client.GetStream()); } }
/// <summary> /// Cancels the specified file-transfer. /// </summary> /// <param name="transfer">The file-transfer to cancel.</param> /// <exception cref="ArgumentNullException">The transfer parameter is /// null.</exception> /// <exception cref="ArgumentException">The specified transfer instance does /// not represent an active data-transfer operation.</exception> public void CancelFileTransfer(FileTransfer transfer) { transfer.ThrowIfNull("transfer"); SISession session = GetSession(transfer.SessionId, transfer.From, transfer.To); if (session == null) { throw new ArgumentException("The specified transfer instance does not " + "represent an active data-transfer operation."); } session.Extension.CancelTransfer(session); }
/// <summary> /// Performs the actual data-transfer implied by the specified /// SI session. /// </summary> /// <param name="session">The SI session whose data to transfer.</param> /// <exception cref="ArgumentNullException">The session parameter is /// null.</exception> /// <exception cref="NotSupportedException">The XMPP extension /// implementing this method is not supported by the intended recipient's /// XMPP client.</exception> /// <exception cref="XmppErrorException">The server or the XMPP entity /// with the specified JID returned an XMPP error code. Use the Error /// property of the XmppErrorException to obtain the specific error /// condition.</exception> /// <exception cref="XmppException">The server returned invalid data or /// another unspecified XMPP error occurred.</exception> public void Transfer(SISession session) { session.ThrowIfNull("session"); // Open the negotiated IBB. OpenStream(session.To, session.Sid); byte[] buf = new byte[blockSize]; // 'seq' is defined as 16-bit unsigned short value that wraps around. ushort seq = 0; long left = session.Size; try { while (left > 0) { int read = session.Stream.Read(buf, 0, blockSize); left = left - read; if (read <= 0) { break; } string b64 = Convert.ToBase64String(buf, 0, read); var data = Xml.Element("data", "http://jabber.org/protocol/ibb") .Attr("sid", session.Sid) .Attr("seq", seq.ToString()) .Text(b64); seq++; Iq response = im.IqRequest(IqType.Set, session.To, im.Jid, data); if (response.Type == IqType.Error) { throw Util.ExceptionFromError(response); } session.Count = session.Count + read; // Raise the 'BytesTransferred' event. BytesTransferred.Raise(this, new BytesTransferredEventArgs(session)); } } catch (ObjectDisposedException) { // This means the IO-stream has been disposed because we cancelled // the transfer. Just fall through. } catch { // The IQ response is of type 'error', the other site has cancelled // the transfer. TransferAborted.Raise(this, new TransferAbortedEventArgs(session)); // Rethrow. throw; } finally { // Gracefully close the IBB. CloseStream(session.To, session.Sid); } }
/// <summary> /// Performs the actual data-transfer implied by the specified /// SI session. /// </summary> /// <param name="session">The SI session whose data to transfer.</param> /// <exception cref="ArgumentNullException">The session parameter is /// null.</exception> /// <exception cref="NotSupportedException">The XMPP extension /// implementing this method is not supported by the intended recipient's /// XMPP client.</exception> /// <exception cref="XmppErrorException">The server or the XMPP entity /// with the specified JID returned an XMPP error code. Use the Error /// property of the XmppErrorException to obtain the specific error /// condition.</exception> /// <exception cref="XmppException">The server returned invalid data or /// another unspecified XMPP error occurred.</exception> public void Transfer(SISession session) { IEnumerable <Streamhost> proxies = null; // Determine if this is going to be a direct or a mediated transfer. if (ProxyAllowed) { try { var externalAddresses = GetExternalAddresses(); // If all of our external addresses are behind NATs, we may need a proxy. bool behindNAT = externalAddresses.All(addr => BehindNAT(addr)); if (behindNAT) { // Look for user-defined proxies first. if (Proxies.Count > 0) { proxies = Proxies; } else { // Otherwise query XMPP server for a list of proxies. proxies = GetProxyList(); } } } catch { // Retrieving the external addresses may fail as well as querying // the XMPP server for proxies. In these cases, we at least try // with a direct transfer. } } try { if (proxies != null && proxies.Count() > 0) { MediatedTransfer(session, proxies); } else { DirectTransfer(session); } } catch (Exception) { // Raise the 'TransferAborted' event. TransferAborted.Raise(this, new TransferAbortedEventArgs(session)); // Invalidate the session. siFileTransfer.InvalidateSession(session.Sid); } }
/// <summary> /// Negotiates with the target which of the specified SOCKS5 proxies to use. /// </summary> /// <param name="session">The SI session whose data to transfer.</param> /// <param name="proxies">An enumerable collection of SOCKS5 proxy servers /// to advertise to the target.</param> /// <returns>The proxy server to use for the data transfer.</returns> /// <exception cref="XmppErrorException">The server returned an XMPP error code. /// Use the Error property of the XmppErrorException to obtain the specific /// error condition.</exception> /// <exception cref="XmppException">The server returned invalid data or another /// unspecified XMPP error occurred.</exception> Streamhost NegotiateProxy(SISession session, IEnumerable <Streamhost> proxies) { // Compile XML containing our list of proxy servers. var xml = Xml.Element("query", "http://jabber.org/protocol/bytestreams") .Attr("sid", session.Sid); foreach (var proxy in proxies) { xml.Child(Xml.Element("streamhost") .Attr("jid", proxy.Jid.ToString()) .Attr("host", proxy.Host) .Attr("port", proxy.Port.ToString())); } // Wait for the other site to tell us which proxy server it selected. var iq = im.IqRequest(IqType.Set, session.To, im.Jid, xml); if (iq.Type == IqType.Error) { throw Util.ExceptionFromError(iq, "The SOCKS5 negotiation failed."); } var query = iq.Data["query"]; if (query == null || query.NamespaceURI != "http://jabber.org/protocol/bytestreams") { throw new XmppException("Erroneous response."); } if (query.GetAttribute("sid") != session.Sid) { throw new XmppException("Invalid session identifier."); } var used = query["streamhost-used"]; if (used == null) { throw new XmppException("Missing streamhost-used element."); } var proxyJid = used.GetAttribute("jid"); var streamhost = proxies.FirstOrDefault(proxy => proxy.Jid == proxyJid); if (streamhost == null) { throw new XmppException("Invalid streamhost JID."); } return(streamhost); }
/// <summary> /// Processes an IBB 'close' request. /// </summary> /// <param name="sessionId">The mandatory session id attribute of the 'close' /// element.</param> /// <param name="stanza">The IQ stanza containing the request.</param> /// <exception cref="ArgumentNullException">The sessionId parameter or the /// stanza parameter is null.</exception> void Close(string sessionId, Iq stanza) { sessionId.ThrowIfNull("sessionId"); stanza.ThrowIfNull("stanza"); SISession session = siFileTransfer.GetSession(sessionId, stanza.From, stanza.To); // We don't allow the other site to close a session that we opened. if (session != null) { siFileTransfer.InvalidateSession(sessionId); // Had all bytes been received when we got the 'close' request? // Otherwise, the other site cancelled the transfer prematurely. if (session.Count < session.Size) { TransferAborted.Raise(this, new TransferAbortedEventArgs(session)); } } }
/// <summary> /// Receives the actual data after SOCKS5 negotiation has been completed. /// </summary> /// <param name="stanza">The original requesting IQ stanza.</param> /// <param name="sid">The session-id associated with the /// data-transfer.</param> /// <param name="stream">The stream from which to read the incoming /// data.</param> /// <exception cref="XmppException">The specifid SID does not denote a /// valid SI session.</exception> void ReceiveData(Iq stanza, string sid, Stream stream) { SISession session = siFileTransfer.GetSession(sid, stanza.From, stanza.To); if (session == null) { throw new XmppException("Invalid session-id: " + sid); } long left = session.Size; try { while (left > 0) { byte[] buffer = new byte[4096]; int read = stream.Read(buffer, 0, buffer.Length); if (read <= 0) { break; } left = left - read; session.Stream.Write(buffer, 0, read); // Update the byte count and raise the 'BytesTransferred' event. session.Count = session.Count + read; BytesTransferred.Raise(this, new BytesTransferredEventArgs(session)); } } catch (ObjectDisposedException) { // This means the IO-stream has been disposed because we cancelled // the transfer. Just fall through. } finally { // Tear down the SI session. siFileTransfer.InvalidateSession(sid); // If not all bytes have been transferred, the data-transfer must have // been aborted prematurely. if (session.Count < session.Size) { TransferAborted.Raise(this, new TransferAbortedEventArgs(session)); } } }
/// <summary> /// Invoked whenever a 'Stream Initiation' request for file transfers /// is received. /// </summary> /// <param name="from">The JID of the XMPP entity that wishes to initiate /// the data-stream.</param> /// <param name="si">The 'si' element of the request.</param> /// <returns>The response to the SI request or an error element to include /// in the IQ response.</returns> XmlElement OnStreamInitiationRequest(Jid from, XmlElement si) { try { string method = SelectStreamMethod(si["feature"]); // If the session-id is already in use, we cannot process the request. string sid = si.GetAttribute("id"); if (String.IsNullOrEmpty(sid) || siSessions.ContainsKey(sid)) { return(new XmppError(ErrorType.Cancel, ErrorCondition.Conflict).Data); } // Extract file information and hand to user. var file = si["file"]; string desc = file["desc"] != null ? file["desc"].InnerText : null, name = file.GetAttribute("name"); int size = Int32.Parse(file.GetAttribute("size")); FileTransfer transfer = new FileTransfer(from, im.Jid, name, size, sid, desc); string savePath = TransferRequest.Invoke(transfer); // User has rejected the request. if (savePath == null) { return(new XmppError(ErrorType.Cancel, ErrorCondition.NotAcceptable).Data); } // Create an SI session instance. SISession session = new SISession(sid, File.OpenWrite(savePath), size, true, from, im.Jid, im.GetExtension(method) as IDataStream); siSessions.TryAdd(sid, session); // Store the file's meta data. metaData.TryAdd(sid, new FileMetaData(name, desc)); // Construct and return the negotiation result. return(Xml.Element("si", "http://jabber.org/protocol/si").Child( FeatureNegotiation.Create(new SubmitForm( new ListField("stream-method", method))))); } catch { return(new XmppError(ErrorType.Cancel, ErrorCondition.BadRequest).Data); } }
/// <summary> /// Sends the actual data associated with the specified session over the /// specified stream. /// </summary> /// <param name="session">The SI session whose data to transfer.</param> /// <param name="stream">The (network) stream representing the connection /// to the client.</param> void SendData(SISession session, Stream stream) { long left = session.Size; try { while (left > 0) { byte[] buffer = new byte[4096]; int read = session.Stream.Read(buffer, 0, (int)Math.Min(left, buffer.Length)); if (read > 0) { stream.Write(buffer, 0, read); } else { break; } left = left - read; // Update the byte count and raise the 'BytesTransferred' event. session.Count = session.Count + read; BytesTransferred.Raise(this, new BytesTransferredEventArgs(session)); } } catch (ObjectDisposedException) { // This means the IO-stream has been disposed because we cancelled // the transfer. Just fall through. } finally { // Tear down the SI session. siFileTransfer.InvalidateSession(session.Sid); // If not all bytes have been transferred, the data-transfer must have // been aborted prematurely. if (session.Count < session.Size) { TransferAborted.Raise(this, new TransferAbortedEventArgs(session)); } } }
/// <summary> /// Invoked once the result of a pending stream-initiation operation has been /// received. /// </summary> /// <param name="result">The result of the stream-initiation operation. If /// this parameter is null, stream-initiation failed.</param> /// <param name="to">The JID of the XMPP user to offer the file to.</param> /// <param name="stream">The stream to read the file-data from.</param> /// <param name="name">The name of the file, as offered to the XMPP user /// with the specified JID.</param> /// <param name="size">The number of bytes to transfer.</param> /// <param name="cb">A callback method invoked once the other site has /// accepted or rejected the file-transfer request.</param> /// <param name="description">A description of the file so the receiver can /// better understand what is being sent.</param> /// <remarks>This is called in the context of an arbitrary thread.</remarks> void OnInitiationResult(InitiationResult result, Jid to, string name, Stream stream, long size, string description, Action <bool, FileTransfer> cb) { FileTransfer transfer = new FileTransfer(im.Jid, to, name, size, null, description); try { // Get the instance of the data-stream extension that the other site has // selected. IDataStream ext = im.GetExtension(result.Method) as IDataStream; // Register the session. SISession session = new SISession(result.SessionId, stream, size, false, im.Jid, to, ext); siSessions.TryAdd(result.SessionId, session); // Store the file's meta data. metaData.TryAdd(result.SessionId, new FileMetaData(name, description)); // Invoke user-provided callback. if (cb != null) { cb.Invoke(true, transfer); } // Perform the actual data-transfer. try { ext.Transfer(session); } catch (Exception e) { // Nothing to do here. } } catch { // Something went wrong. Invoke user-provided callback to let them know // the file-transfer can't be performed. if (cb != null) { cb.Invoke(false, transfer); } } }
/// <summary> /// Performs a direct transfer, meaning we act as a SOCKS5 server. /// </summary> /// <param name="session">The SI session whose data to transfer.</param> /// <exception cref="Socks5Exception">The SOCKS5 server could not be /// instantiated.</exception> /// <exception cref="XmppErrorException">The server returned an XMPP error code. /// Use the Error property of the XmppErrorException to obtain the specific /// error condition.</exception> /// <exception cref="XmppException">The server returned invalid data or another /// unspecified XMPP error occurred.</exception> void DirectTransfer(SISession session) { // Create the listening SOCKS5 server. Socks5Server socks5Server = null; try { socks5Server = CreateSocks5Server(serverPortFrom, serverPortTo); } catch (Exception e) { throw new Socks5Exception("The SOCKS5 server could not be created.", e); } IEnumerable <IPAddress> externalAddresses = null; try { externalAddresses = GetExternalAddresses(); // Check if we might need to forward the server port. /*if (externalAddresses.Any(addr => BehindNAT(addr)) && UseUPnP) { * try { * UPnP.ForwardPort(socks5Server.Port, ProtocolType.Tcp, * "XMPP SOCKS5 File-transfer"); * } catch (InvalidOperationException) { * // If automatic port forwarding failed for whatever reason, just * // go on normally. The user can still configure forwarding manually. * } * }*/ } catch (NotSupportedException) { // Not much we can do. } // Waiting for a client connection is a blocking call and we need to // negotiate the SOCKS5 connection after we send the IQ request but // _before_ we wait for the IQ response. Task.Factory.StartNew(() => { try { AcceptClientConnection(session, socks5Server, acceptTimeout); SendData(session, socks5Server.GetStream()); } finally { socks5Server.Close(); } }); // Send target a list of streamhosts, one for each active network interface. var xml = Xml.Element("query", "http://jabber.org/protocol/bytestreams") .Attr("sid", session.Sid); // Compile a set of all our IP addresses that we can advertise. ISet <IPAddress> ips = new HashSet <IPAddress>(); if (externalAddresses != null) { ips.UnionWith(externalAddresses); } ips.UnionWith(GetIpAddresses()); foreach (var ip in ips) { xml.Child(Xml.Element("streamhost") .Attr("jid", im.Jid.ToString()) .Attr("host", ip.ToString()) .Attr("port", socks5Server.Port.ToString())); } // Send IQ with streamhosts to the target. var iq = im.IqRequest(IqType.Set, session.To, im.Jid, xml); if (iq.Type == IqType.Error) { throw Util.ExceptionFromError(iq, "The SOCKS5 connection could not " + "be established."); } }
/// <summary> /// Cancels the data-transfer implied by the specified SI session. /// </summary> /// <param name="session">The SI session whose data-transfer to /// cancel.</param> /// <exception cref="ArgumentNullException">The session parameter is /// null.</exception> public void CancelTransfer(SISession session) { session.ThrowIfNull("session"); siFileTransfer.InvalidateSession(session.Sid); }
/// <summary> /// Cancels the data-transfer implied by the specified SI session. /// </summary> /// <param name="session">The SI session whose data-transfer to /// cancel.</param> /// <exception cref="ArgumentNullException">The session parameter is /// null.</exception> public void CancelTransfer(SISession session) { session.ThrowIfNull("session"); siFileTransfer.InvalidateSession(session.Sid); TransferAborted.Raise(this, new TransferAbortedEventArgs(session)); }
/// <summary> /// Initializes a new instance of the BytesTransferredEventArgs class. /// </summary> /// <param name="session">The session for which the event is raised.</param> /// <exception cref="ArgumentNullException">The session parameter /// is null.</exception> public TransferAbortedEventArgs(SISession session) { session.ThrowIfNull("session"); Session = session; }
/// <summary> /// Initializes a new instance of the BytesTransferredEventArgs class. /// </summary> /// <param name="session">The session for which the event is raised.</param> /// <exception cref="ArgumentNullException">The session parameter /// is null.</exception> public BytesTransferredEventArgs(SISession session) { session.ThrowIfNull("session"); Session = session; }
/// <summary> /// Initializes a new instance of the FileTransfer class. /// </summary> /// <param name="session">The SISession instance to initialize this /// instance from.</param> /// <param name="name">The name of the file.</param> /// <param name="description">A description of the file.</param> /// <exception cref="ArgumentNullException">The session parameter or /// the name parameter is null.</exception> internal FileTransfer(SISession session, string name, string description) : this(session.From, session.To, name, session.Size, session.Sid, description, session.Count) { }