|
-
Aug 14th, 2009, 10:44 AM
#1
[RESOLVED] Make TCPListener wait for full command
OK I am probably going to completely fail but I thought I would just try making an SMTP server (which came from another thread on here I was posting in earlier). At the moment I am just concentrating on RECEIVING emails via SMTP, I dont care about sending them yet.
So the first thing I want to do is just make my server program be able to receive a simple HELO command from Telnet on port 25. Simple enough eh? Apparently not 
The problem I have is that my program starts reading the data as soon as 1 character is typed into telnet, rather than it waiting for the telnet user to press Enter before it starts to process the command. I've had a look around and looked at Athiest's examples but cant figure out how to make it wait. Here is what I have so far:
vb.net Code:
Private Sub StartBtn_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles StartBtn.Click
Dim bgthread As New Threading.Thread(AddressOf StartServer)
bgthread.IsBackground = True
bgthread.Start()
End Sub
''' <summary>
''' Starts listening on port 25
''' </summary>
Private Sub StartServer()
Dim TListener As New TcpListener(Net.IPAddress.Any, 25)
TListener.Start()
Dim TClient As TcpClient
Do
TClient = TListener.AcceptTcpClient()
Invoke(New Action(Of String)(AddressOf WriteToLog), TClient.Client.RemoteEndPoint.ToString & " Connected")
WriteToClientStream(TClient.GetStream, "Hello from SMTP Server!")
'<This is where I am struggling>
Dim DataAsBytes(1024) As Byte
TClient.GetStream.Read(DataAsBytes, 0, 1024) '<-- This line executes as soon as one character is typed in telnet
Dim DataAsString As String = System.Text.Encoding.ASCII.GetString(DataAsBytes)
Invoke(New Action(Of String)(AddressOf WriteToLog), DataAsString)
'</Struggle>
Loop
End Sub
''' <summary>
''' Writes a message back to the client's stream.
''' If the client is a telnet client then this message will appear in the telnet console
''' </summary>
''' <param name="stream">The underlying stream to write to</param>
''' <param name="msg">The string to write to the stream</param>
Private Sub WriteToClientStream(ByVal stream As IO.Stream, ByVal msg As String)
Dim Writer As New IO.StreamWriter(stream)
Writer.WriteLine(msg)
Writer.Flush()
End Sub
''' <summary>
''' Writes to the log file (currently just an RTB on screen)
''' </summary>
''' <param name="msg">The string to write to the log</param>
Private Sub WriteToLog(ByVal msg As String)
logbox.AppendText(msg & vbNewLine)
End Sub
See the comments in the StartServer sub for info on where exactly the problem is.
Any ideas?
Thanks
Chris
-
Aug 14th, 2009, 11:28 AM
#2
Re: Make TCPListener wait for full command
In your loop you are waiting for a new connection each time. Do something like this:
Code:
Private Sub StartServer()
Dim TListener As New TcpListener(Net.IPAddress.Any, 25)
TListener.Start()
Dim TClient As TcpClient
TClient = TListener.AcceptTcpClient()
Invoke(New Action(Of String)(AddressOf WriteToLog), TClient.Client.RemoteEndPoint.ToString & " Connected")
WriteToClientStream(TClient.GetStream, "Hello from SMTP Server!")
Do
'<This is where I am struggling>
Dim DataAsBytes(1024) As Byte
TClient.GetStream.Read(DataAsBytes, 0, 1024) '<-- This line executes as soon as one character is typed in telnet
Dim DataAsString As String = System.Text.Encoding.ASCII.GetString(DataAsBytes)
Invoke(New Action(Of String)(AddressOf WriteToLog), DataAsString)
'</Struggle>
Loop
End Sub
-
Aug 14th, 2009, 12:08 PM
#3
Re: Make TCPListener wait for full command
OK I'll give that a go thanks, but I do need the original loop as well because once one client has finished its session I need it to go back to waiting for another client to connect.
-
Aug 14th, 2009, 12:16 PM
#4
Re: Make TCPListener wait for full command
Probably what you want to do is have one thread that has a loop that is just listening for clients. When a client connects you launch a new thread to handle the connection for that client. This way you can handle an arbitrary number of connections coming in. If you only had a single loop, you could not accept new connections while another server was connected. If they were sending a large file via email, that could prevent other servers from connecting for a significant period of time.
-
Aug 14th, 2009, 12:33 PM
#5
Re: Make TCPListener wait for full command
OK that makes it read every character thanks It also makes me understand why when you send something to an SMTP server via telnet, if you make a typo and delete it then retype it then the command doesnt work - because it has already stored those values you typed before.
Anywho, next problem is getting it to recognise that the user pressed the Enter key. I thought this would just be like:
vb Code:
Dim DataAsBytes(255) As Byte
TClient.GetStream.Read(DataAsBytes, 0, 255)
Dim ShortDataAsString As String = System.Text.Encoding.ASCII.GetString(DataAsBytes)
If ShortDataAsString = vbCrLf Then
Debug.WriteLine("end of command")
Exit Do
End If
but that IF statement always evaluates false. I tried vbCr and vbNewLine but nothing. When I step through the code and look at the value of ShortDataAsString it just shows a single speech mark ( " not ' ) and I have no idea how to match this to something I can produce in code... I'll keep working on it but if anyone has any suggestions that would be good
-
Aug 14th, 2009, 12:33 PM
#6
Re: Make TCPListener wait for full command
 Originally Posted by Negative0
Probably what you want to do is have one thread that has a loop that is just listening for clients. When a client connects you launch a new thread to handle the connection for that client. This way you can handle an arbitrary number of connections coming in. If you only had a single loop, you could not accept new connections while another server was connected. If they were sending a large file via email, that could prevent other servers from connecting for a significant period of time.
yeah I'm going to do that when I get it all working but for now I just want to have one thread to keep things simple
-
Aug 14th, 2009, 12:40 PM
#7
Re: Make TCPListener wait for full command
OK I figured I can just do this (and it works) but can someone just confirm this is a reliable way of checking for the Enter key being pressed and sent to my server?
vb Code:
Dim DataAsBytes(8) As Byte
TClient.GetStream.Read(DataAsBytes, 0, 8)
If DataAsBytes(0) = 13 AndAlso DataAsBytes(1) = 10 Then
Debug.WriteLine("end of command")
Exit Do
End If
I dont know much about Byte arrays / characters so I dont know if its possible for some other character (or character combination) to possibly have 13 as the first byte and 10 as the second byte as well...
Also, I assume I can have a really small byte array like the 8 byte long array I have created in the above code because it is only one character being passed to the server at a time?
EDIT: apoligies for the tripple post! and for starting pretty much every post with "OK"
-
Aug 14th, 2009, 12:52 PM
#8
Re: Make TCPListener wait for full command
You are probably better off getting the size from the Read method and then only reading those characters in:
Code:
size = TClient.GetStream.Read(DataAsBytes, 0, 20) '<-- This line executes as soon as one character is typed in telnet
Dim DataAsString As String = System.Text.Encoding.ASCII.GetString(DataAsBytes, 0, size)
If you don't specify the size, then it will convert the non read bytes to null strings, and your string would always be 1024 characters. The code above trims it down just to the size of the data read.
-
Aug 14th, 2009, 01:09 PM
#9
Fanatic Member
Re: Make TCPListener wait for full command
Code:
An SMTP data (or d8ta) line is a sequence of 0 to 1000 octets,
including the sequence 0x0C (CR) 0x0A (LF) which ends the line. A CR
without a following LF, or an LF without a preceeding CR, is not a
line delimiter, and must not be interpreted as one, regardless of the
local system's line delimiter convention.
SMTP clients MUST NOT transmit lines of length greater than 1000
octets. It is RECOMMENDED that SMTP server implementations impose no
limit on the maximum number of octets in a line.
-
Aug 14th, 2009, 01:35 PM
#10
Re: Make TCPListener wait for full command
That is the DATA command it is talking about. I havent got that far yet. DATA is the command you type after you have used RCPT TO, FROM and SUBJECT to specify the sender, recipient and subject of your email to the SMTP server - you type DATA and then type your email message, followed by two CrLF's to signal that you have finished typing the email body.
Its going quite well so far though, clients can connect, send the HELO message and the server will respond, or they can type QUIT and it will drop their connection 
Current code for any interested parties (havent finished commenting it yet but oh well) :
vb.net Code:
Private Sub StartBtn_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles StartBtn.Click
Dim bgthread As New Threading.Thread(AddressOf StartServer)
bgthread.IsBackground = True
bgthread.Start() 'Start new background thread that runs the StartServer Sub
StartBtn.Enabled = False
Stopbtn.Enabled = True
End Sub
''' <summary>
''' Starts listening on port 25 and passes incoming requests to a ThreadPool thread for processing
''' </summary>
Private Sub StartServer()
Threading.ThreadPool.SetMaxThreads(40, 300) 'Set the maximum amount of threads the ThreadPool can create for us simultaneously
Dim TListener As New TcpListener(Net.IPAddress.Any, 25) 'Create the listener that will accept connections on port 25
TListener.Start() 'Start the listener
'When a new client connection is accepted, start the StartSession Sub on a new ThreadPool thread
'and then loop back to get ready for new client to connect
Do
Threading.ThreadPool.QueueUserWorkItem(AddressOf StartSession, TListener.AcceptTcpClient)
Loop
End Sub
''' <summary>
''' Handles the session and command processing for each connected client
''' </summary>
''' <param name="AcceptedTcpClient">The TcpClient object that represents the connected client</param>
Private Sub StartSession(ByVal AcceptedTcpClient As Object)
Dim TClient As TcpClient = DirectCast(AcceptedTcpClient, TcpClient)
Dim RemoteIP As String = TClient.Client.RemoteEndPoint.ToString
WriteToClientStream(TClient.GetStream, Now.ToShortTimeString & " - Prototype SMTP Server")
BG_WriteToLog(RemoteIP & " connected at " & Now.ToString)
Dim strbuild As New StringBuilder
Dim ExitSession As Boolean = False
Dim CommandEntered As String = String.Empty
Do While ExitSession = False
Do
Dim size As Int32 = 0
Dim DataAsBytes(10) As Byte
size = TClient.GetStream.Read(DataAsBytes, 0, 10)
If DataAsBytes(0) = 13 AndAlso DataAsBytes(1) = 10 Then
Exit Do
End If
Dim ShortDataAsString As String = System.Text.Encoding.ASCII.GetString(DataAsBytes, 0, size)
strbuild.Append(ShortDataAsString)
Loop 'Start listening for new data on the stream again
CommandEntered = strbuild.ToString
BG_WriteToLog(RemoteIP & " : " & CommandEntered)
If String.Compare(CommandEntered, "HELO", True) = 0 Then
WriteToClientStream(TClient.GetStream, "HELO From " & TClient.Client.LocalEndPoint.ToString)
ElseIf String.Compare(CommandEntered, "QUIT", True) = 0 Then
ExitSession = True
TClient.Close()
Else
WriteToClientStream(TClient.GetStream, "Unknown Command: " & CommandEntered)
End If
strbuild.Remove(0, strbuild.Length) 'Clear out the command string ready for next command
Loop 'Start processing commands that are sent from the client again
End Sub
''' <summary>
''' Writes a message back to the client's stream.
''' If the client is a telnet client then this message will appear in the telnet console
''' </summary>
''' <param name="stream">The underlying stream to write to</param>
''' <param name="msg">The string to write to the stream</param>
Private Sub WriteToClientStream(ByVal stream As IO.Stream, ByVal msg As String)
Dim Writer As New IO.StreamWriter(stream)
Writer.WriteLine(msg)
Writer.Flush()
End Sub
''' <summary>
''' Writes to the log file and to the textbox on the screen.
''' Intended to be executed on the UI thread
''' </summary>
''' <param name="msg">The string to write to the log</param>
Private Sub UI_WriteToLog(ByVal msg As String)
logbox.AppendText(msg & vbNewLine)
End Sub
''' <summary>
''' Invokes a delegate to execute the UI_WriteToLog Sub.
''' Intended to be executed from a background thread.
''' </summary>
''' <param name="msg">The string to write to the log</param>
Private Sub BG_WriteToLog(ByVal msg As String)
Me.Invoke(New Action(Of String)(AddressOf UI_WriteToLog), msg)
End Sub
-
Aug 14th, 2009, 01:39 PM
#11
Fanatic Member
Re: Make TCPListener wait for full command
correct,
was a snippet of the standard labeled 'smtp line delimiter'.
each line should ended in this fashion.
-
Aug 14th, 2009, 01:46 PM
#12
Re: Make TCPListener wait for full command
Yes but thats only the DATA command part... Thats nothing to do with what I was talking about earlier with not being able to detect the user pressing Enter after each command (which I assume is why you posted it, after all you didnt really give any explanation or anything as to what it was relating to..)
-
Aug 14th, 2009, 02:01 PM
#13
Fanatic Member
Re: Make TCPListener wait for full command
Am I incorrect in understanding the RFC indicating \r\n for every line including HELO, this would coincidentally be the enter key you were searching for earlier?
-
Aug 14th, 2009, 11:14 PM
#14
Re: [RESOLVED] Make TCPListener wait for full command
Marking this as resolved as the original question was resolved by Negative0 - Thanks!
Posting Permissions
- You may not post new threads
- You may not post replies
- You may not post attachments
- You may not edit your posts
-
Forum Rules
|
Click Here to Expand Forum to Full Width
|