caml-list - the Caml user's mailing list
 help / color / mirror / Atom feed
From: Gerd Stolpmann <info@gerd-stolpmann.de>
To: Andrei Errapart <andreie@no.spam.ee>
Cc: caml-list@inria.fr
Subject: Re: [Caml-list] calling telnet from a caml program
Date: Wed, 6 Nov 2002 21:56:48 +0100	[thread overview]
Message-ID: <20021106205648.GA1038@ice.gerd-stolpmann.de> (raw)
In-Reply-To: <Pine.LNX.4.44.0211061910030.1661-100000@no.spam.ee>; from andreie@no.spam.ee on Mit, Nov 06, 2002 at 18:20:05 +0100

[-- Attachment #1: Type: text/plain, Size: 970 bytes --]


Am 2002.11.06 18:20 schrieb(en) Andrei Errapart:
> On Wed, 6 Nov 2002, Alan Schmitt wrote:
> ...
> > That's what I was afraid of ... So on to the next question: is it
> > possible to use pseudy-ttys with caml ? Or should I start thinking about
> > reimplementing the telnet client protocol ...
> 
> If it is possible in C, then it is possible in OCaml, too. Most 
> probably some termios(3) calls are needed, but not much more. By the 
> way, Gerd Stolpmann mentions telnet client in his Netclient library.

Yes, there is a telnet client in netclient version 0.90.1. I have attached
the MLI interface, and a simple example.

You can download netclient at
http://www.gerd-stolpmann.de/packages/netclient-0.90.1.tar.gz

Gerd

------------------------------------------------------------
Gerd Stolpmann * Viktoriastr. 45 * 64293 Darmstadt * Germany 
gerd@gerd-stolpmann.de          http://www.gerd-stolpmann.de
------------------------------------------------------------

[-- Attachment #2: telnet_client.mli --]
[-- Type: text/plain, Size: 14226 bytes --]

(* $Id: telnet_client.mli,v 1.2 2001/09/05 21:41:48 gerd Exp $
 * ----------------------------------------------------------------------
 *
 *)

(* This is a Telnet client providing the basic Telnet services. It
 * supports sending and receiving data (asynchronously), and the
 * negotiation of Telnet options, but it does not implement any option.
 *)

exception Telnet_protocol of exn;;
(* Wrapper for exceptions that already passed the exception handler. *)


type telnet_command =
    Telnet_data of string
  | Telnet_nop
  | Telnet_dm           (* data mark *)
  | Telnet_brk          (* break *)
  | Telnet_ip           (* interrupt process *)
  | Telnet_ao           (* abort output *)
  | Telnet_ayt          (* are you there? *)
  | Telnet_ec           (* erase character *)
  | Telnet_el           (* erase line *)
  | Telnet_ga           (* Go ahead *)
  | Telnet_sb of char   (* Begin of subnegotiation *)
  | Telnet_se           (* End of subnegotation *)
  | Telnet_will of char (* Acknowledges that option is in effect *)
  | Telnet_wont of char (* Acknowledges that option is rejected *)
  | Telnet_do of char   (* Requests to turn on an option *)
  | Telnet_dont of char (* Requests to turn off an option *)
  | Telnet_unknown of char (* Unknown command *)
  | Telnet_eof          (* End of file *)
  | Telnet_timeout      (* Timeout event *)
;;

(* A telnet_command is the interpretation of the octets in a Telnet
 * session, i.e. it is one level above the octet stream. See RFC854
 * for an explanation what the commands mean. Telnet_data represents
 * the data chunks between the commands. Note that you do not need
 * to double octets having value 255; this is done automatically.
 * Telnet_unknown represents any command not covered by RFC854, for
 * example the End-of-record-mark (introduced in RFC885) would be
 * Telnet_unknown '\239'. Telnet_eof represents the end of the octet
 * stream, useable in both directions. Telnet_timeout is added to the
 * input queue if I/O has not been happened for the configured period
 * of time.
 *)

type telnet_options =
    { connection_timeout : float;
      verbose_connection : bool;
      verbose_input : bool;
      verbose_output : bool;
    }
;;

(* telnet_options: modifies the behaviour of the client. Do not mix these
 * options up with the options negotiated with the remote side.
 *
 * 'connection_timeout':     After this period of time (in seconds) a
 *                           Telnet_timeout pseudo-command is added to
 *                           the input queue, and the connection is
 *                           aborted.
 * 'verbose_connection':     Enables printing of connection events to stderr.
 * 'verbose_input':          Enables printing of input events to stderr.
 * 'verbose_output':         Enables printing of output events to stderr.
 *)


type telnet_negotiated_option =
    Telnet_binary       (* see RFC 856 *)
  | Telnet_echo         (* see RFC 857 *)
  | Telnet_suppress_GA  (* see RFC 858 *)
  | Telnet_status       (* see RFC 859 *)
  | Telnet_timing_mark  (* see RFC 860 *)
  | Telnet_ext_opt_list (* see RFC 861 *)
  | Telnet_end_of_rec   (* see RFC 885 *)
  | Telnet_window_size  (* see RFC 1073 *)
  | Telnet_term_speed   (* see RFC 1079 *)
  | Telnet_term_type    (* see RFC 1091 *)
  | Telnet_X_display    (* see RFC 1096 *)
  | Telnet_linemode     (* see RFC 1184 *)
  | Telnet_flow_ctrl    (* see RFC 1372 *)
  | Telnet_auth         (* see RFC 1416 *)
  | Telnet_new_environ  (* see RFC 1572 and 1571 *)
  | Telnet_option of int   (* all other options *)
;;

(* telnet_negotiated_option: names for the most common options, and
 * the generic name Telnet_option for other options.
 *)

type telnet_option_state =
    Not_negotiated
  | Accepted
  | Rejected
;;

(* An option has one of three states:
 * - Not_negotiated: There was no negotiation about the option. This means
 *   that the option is turned off (but this client is allowed to reject
 *   it explicitly)
 * - Accepted: Both sides have accepted the option.
 * - Rejected: One side has rejected the option. This also means that the
 *   option is off, but the client refuses to send further acknoledgements
 *   that the option is off (to avoid endless negotiation loops).
 *)


val char_of_option : telnet_negotiated_option -> char
(* Converts the option name to the character representing it on the
 * octet-stream level.
 *)

val option_of_char : char -> telnet_negotiated_option
(* Converts a character representing an option to the internal option
 * name.
 *)


type telnet_connector =
    Telnet_connect of (string * int)
  | Telnet_socket of Unix.file_descr
;;

(* telnet_connector:
 * Telnet_connect(host,port): The client connects to this port.
 * Telnet_socket s: The client uses an already connected socket.
 *
 * Why Telnet_socket? Telnet is a symmetrical protocol; client and servers
 * implement the same protocol features (the only difference is the
 * environment: a client is typically connected with a real terminal; a server
 * is connected with a pseudo terminal). This simply means that this
 * implementation of a CLIENT can also be used as a SERVER implementation.
 * You need only to add code which accepts new connections and which passes
 * these connections over to a telnet_session object via Telnet_socket.
 *)


class telnet_session :
  object
    (* GENERAL:
     *
     * The telnet_session object has two queues, one for arriving data,
     * one for data to send.
     * Once the session object is attached to an event system, it connects
     * to the remote peer, and processes the queues. Input is appended to
     * the input queue; output found on the output queue is sent to the
     * other side.
     * If input arrives, a callback function is invoked.
     * You may close the output side of the socket by putting Telnet_eof
     * onto the output queue.
     * Once the EOF marker has been received, a Telnet_eof is appended to
     * the input queue, and the connection is closed (completely). The
     * session object detaches from the event system automatically in this
     * case.
     * 
     * HOW TO USE THE SESSION OBJECT:
     *
     * Pass an input handler as callback function to the session object.
     * The input handler is called when new input data have been arrived.
     * It should inspect the input queue, process the queue as much as
     * possible, and it should remove the processed items from the queue.
     * While processing, it may add new items to the output queue. 
     *
     * If you are not within the callback function and add items to the
     * output queue, the session object will not detect that there are
     * new items to send - unless you invoke the "update" method.
     *
     * If you want option negotiation, it is the simplest way to use
     * the special option negotiation methods. Configure the options
     * as you want (invoking "enable", "disable" etc), but do not forget
     * to modify the way input is processed. Every Telnet_will, _wont,
     * _do, and _dont command must be passed to process_option_command
     *)

    method set_connection : telnet_connector -> unit
	(* Sets the host name and the port of the remote server to contact. *)

    method set_event_system : Unixqueue.event_system -> unit
	(* Sets the event system to use. By default, a private event system
	 * is used.
	 *)

    method set_callback : (bool -> unit) -> unit
	(* Sets the callback function. This function is called after new
	 * commands have been put onto the input queue. 
	 * The argument passed to the callback function indicates whether
	 * a 'Synch' sequence was received from the remote side or not.
	 *
	 * NOTE SYNCH: If the client sees a data mark command it will assume
	 * that it is actually a Synch sequence. The client automatically
	 * discards any Telnet_data commands from the input queue (but not
	 * Telnet_datas inside subnegotiations). The data mark command
	 * itself remains on the queue.
	 *)

    method set_exception_handler : (exn -> unit) -> unit
	(* Sets the exception handler. Every known error condition is
	 * caught and passed to the exception handler.
	 * The exception handler can do whatever it wants to do. If it
	 * raises again an exception, the new exception is always propagated
	 * up to the caller (whoever this is). Often the caller is the
	 * event system scheduler (i.e. Unixqueue.run); see the documention
	 * there.
	 * If you do not set the exception handler, a default handler is
	 * active. It first resets the session (see method "reset"), and
	 * then wraps the exception into the Telnet_protocol exception,
	 * and raises this exception again.
	 *)

    method output_queue : telnet_command Queue.t
        (* The queue of commands to send to the remote side. If you add new
	 * commands to this queue, do not forget to invoke the 'update'
	 * method which indicates to the event system that new data to
	 * send is available.
	 * After commands have been sent, they are removed from the queue.
	 *)

    method input_queue : telnet_command Queue.t
        (* The queue of commands received from the remote side. This class
	 * only adds commands to the queue (and invokes the callback 
	 * function). The user of this class is responsible for removing
	 * commands from the queue which have been processed.
	 *)

    method get_options : telnet_options
        (* Get the configuration options. *)

    method set_options : telnet_options -> unit
	(* Set the configuration options. *)

    method reset : unit -> unit 
        (* Closes the connection immediately and empties all queues.
	 * All negotiated options are reset, too.
	 *)

    (* The following methods deal with Telnet protocol options. These
     * are negotiated between local and remote side by the Will, Won't,
     * Do and Don't commands. 
     * The "local" options describe the modification of the behaviour
     * of the local side; the "remote" options describe the modifications
     * of the remote side. Both set of options are independent.
     * This object may track the set of accepted and rejected options
     * if the following methods are used; but this works only if
     * the Telnet_will, _wont, _do, and _dont commands received from
     * the remote side are processed by 'process_option_command'. So
     * you need to invoke this method for the mentioned commands in
     * your command interpretation loop.
     * The idea is: If you ENABLE an option, it is possible to
     * switch it on. If the remote side requests an enabled option,
     * the request will be acknowledged. If the remote side does not
     * request an enabled option, it remains off.
     * You can also actively demand an option (OFFER_local_option,
     * REQUEST_remote_option); this is of course only possible if
     * the option is already enabled. In this case the client tries
     * actively to switch it on.
     * You can also DISABLE an option. If the option is 'on', the
     * client actively rejects the option; following the Telnet protocol
     * this is always possible (rejections cannot be rejected).
     * The "reset" methods are somewhat dangerous. They simply reset
     * the internal state of the client, but do not negotiate. This
     * possibility was added to allow the Timing Mark option to send
     * again timing marks even if the previous timing marks have
     * already been accepted. After "reset", the client thinks the
     * option was never negotiated; but nothing is done to tell
     * the remote side about this.
     * option_negotiation_is_over: true if no option negotiation is
     * pending (i.e. nothing has still to be acknowledged).
     *)
    method enable_local_option : telnet_negotiated_option -> unit
    method enable_remote_option : telnet_negotiated_option -> unit
    method disable_local_option : telnet_negotiated_option -> unit
    method disable_remote_option : telnet_negotiated_option -> unit
    method offer_local_option : telnet_negotiated_option -> unit
    method request_remote_option : telnet_negotiated_option -> unit
    method reset_local_option : telnet_negotiated_option -> unit
    method reset_remote_option : telnet_negotiated_option -> unit
    method get_local_option : telnet_negotiated_option -> telnet_option_state
    method get_remote_option : telnet_negotiated_option -> telnet_option_state
    method option_negotiation_is_over : bool
    method process_option_command : telnet_command -> unit

    method fetch_subnegotiation : string option
      (* This method should be called as follows:
       * If you find a Telnet_sb at the beginning of the input queue,
       * remove this command (Queue.take), and invoke fetch_subnegotiation.
       * This method scans the queue and looks for the associated 
       * Telnet_se command. If it does not find it, None is returned.
       * If Telnet_se is found, the parameter enclosed by the two commands
       * is returned as Some s where s is the parameter string. Furthermore,
       * in the latter case the data items and the closing Telnet_se are
       * removed from the queue.
       *)

    (* --- *)



    method attach : unit -> unit
        (* Attach to the event system. After being attached, the client
	 * is ready to work.
	 *)

    method run : unit -> unit
	(* Run the event system *)

    method update : unit -> unit
        (* If there are commands in the output queue, the event system is
	 * signaled that this client wants to do network I/O.
	 *)

    method send_synch : telnet_command list -> unit
        (* At the next output oppurtunity, a Synch sequence is sent to
	 * the remote peer. This means that the passed commands, extended
	 * by an additional Data Mark command, are sent to the peer as
	 * urgent data.
	 * Sending a Synch sequence has higher priority than the output
	 * queue; processing of the output queue is deferred until the
	 * Synch sequence has been completely sent.
	 *)
  end
;;


(* ======================================================================
 * History:
 * 
 * $Log: telnet_client.mli,v $
 * Revision 1.2  2001/09/05 21:41:48  gerd
 * 	Fixed types.
 *
 * Revision 1.1  2000/02/18 01:42:32  gerd
 * 	Initial revision.
 *
 * 
 *)

[-- Attachment #3: telnet.ml --]
[-- Type: text/plain, Size: 4603 bytes --]

#require "netclient";;

(* This is an example for the telnet client. The function below
 * connects with localhost, and logs the user in. It simulates
 * keyboard typing for the username and the password, and finally
 * starts the command "ls".
 *
 * The example may or may not work with your version of telnetd.
 * The program expects the string "login" before the user name must
 * be typed in, and it expects the string "password" before the password
 * must be entered. Furthermore, a new command line is recognized 
 * by the characters >, # or $.
 *)

open Telnet_client;;

type state =
    Start               (* just connected *)
  | Username_sent       (* the user name has been sent to the server *)
  | Password_sent       (* the password has been sent to the server *)
  | Command_sent        (* the command to execute has been sent to the server *)
;;


let login_re = Str.regexp_case_fold "\\(.\\|\n\\)*login";;
let passwd_re = Str.regexp_case_fold "\\(.\\|\n\\)*password";;
let cmd_re = Str.regexp "\\(.\\|\n\\)*[>$#]";;


let login_and_ls username password =
  (* Create a new event system, and the telnet session. We need the 
   * event system only to call Unixqueue.once.
   *)
  let esys = Unixqueue.create_unix_event_system() in
  let session = new telnet_session in
  let state = ref Start in

  let send_string s new_state =
    (* Emulate keyboard typing of the string s. Between the characters there
     * is a delay of 0.1 seconds.
     * When the string has been completely sent, change the state to
     * new_state.
     *)
    let t = ref 0.1 in
    let g = Unixqueue.new_group esys in
    let l = String.length s in
    for i = 0 to l - 1 do
      let c = s.[i] in
      let cs = if c = '\n' then "\r\n" else String.make 1 c in
      (* Do the function !t seconds in the future: *)
      Unixqueue.once esys g !t
	(fun () ->
	   Queue.add (Telnet_data cs) session#output_queue;
	   (* We must call update because we are outside of the regular
	    * callback function. Otherwise the session object would not
	    * notice that the queue has been extended.
	    *)
	   session # update();   
	   if i = l-1 then 
	     state := new_state
	);
      t := !t +. 0.1;
    done
  in

  let got_input is_urgent =
    (* This is the callback function. The session object calls it when
     * telnet commands have been added to the input queue.
     *)
    let oq = session # output_queue in
    let iq = session # input_queue in
    (* Process the input queue command by command: *)
    while Queue.length iq > 0 do
      let cmd = Queue.take iq in
      match cmd with
	| Telnet_will _
	| Telnet_wont _
	| Telnet_do _
	| Telnet_dont _ ->
	    (* These are the commands used to negotiate the telnet options.
	     * The session object can do it for you.
	     *)
	    session # process_option_command cmd
	| Telnet_data s ->
	    (* The data string s has been received. *)
	    ( match !state with
		  Start ->
		    if Str.string_match login_re s 0 then begin
		      (* Assume the host wants our username, and send it. *)
		      send_string (username ^ "\n") Username_sent
		    end
		| Username_sent ->
		    if Str.string_match passwd_re s 0 then begin
		      (* Assume the host wants our password, and send it. *)
		      send_string (password ^ "\n") Password_sent;
		    end
		| Password_sent ->
		    if Str.string_match cmd_re s 0 then begin
		      (* Assume the host wants the command: *)
		      (* Disable now echoing: *)
		      session # disable_remote_option Telnet_echo;
		      (* Send the command "ls" 0.1 seconds in the future.
		       * This way Telnet_echo can be disabled in the
		       * meantime.
		       *)
		      let g = Unixqueue.new_group esys in
		      Unixqueue.once esys g 0.1
			(fun () ->
			   Queue.add (Telnet_data("ls\n")) oq;
			   session # update();
			   state := Command_sent
			)
		    end;
		| Command_sent ->
		    print_string s;
		    (* Again the command-line prompt? *)
		    if Str.string_match cmd_re s 0 then
		      Queue.add Telnet_eof oq;       (* terminate the session *)
	    )
	| Telnet_eof ->
	    ()
	| _ ->
	    (* Unexpected command. *)
	    ()
    done
  in

  session # set_event_system esys;
  let opts = session # get_options in
  session # set_options { opts with 
			    verbose_connection = false;
			    verbose_input = false;
			    verbose_output = false };

  session # set_connection (Telnet_connect("localhost", 23));
  session # enable_remote_option Telnet_suppress_GA;
  session # enable_remote_option Telnet_echo;
  session # set_callback got_input;
  session # attach();
  session # run()
;;

  parent reply	other threads:[~2002-11-06 20:57 UTC|newest]

Thread overview: 8+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2002-11-06 16:11 Alan Schmitt
2002-11-06 16:26 ` Andrei Errapart
2002-11-06 16:34   ` Alan Schmitt
2002-11-06 17:20     ` Andrei Errapart
2002-11-06 17:39       ` Nicolas George
2002-11-06 19:51         ` David Brown
2002-11-06 20:56       ` Gerd Stolpmann [this message]
2002-11-06 21:07 ` Alan Schmitt

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20021106205648.GA1038@ice.gerd-stolpmann.de \
    --to=info@gerd-stolpmann.de \
    --cc=andreie@no.spam.ee \
    --cc=caml-list@inria.fr \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).