FTPS with PL/SQL

Doing a FTP-job with PL/SQL is not difficult.
A basic implementation of RFC 959 can be written in a few hundred lines. See for instance ORACLE-BASE, How to FTP with Oracle PL/SQL or Oracle FAQ’s
But what if you want to secure your FTP transmission. Google doesn’t find any pure PL/SQL solutions, only java solutions with a PL/SQL wrapper.
Securing FTP transmission can be done in two ways: SFTP and FTPS.
SFTP stands for SSH File Transfer Protocol. This protocol is not based on the plain FTP protocol, implementing this in PL/SQL is not more difficult, it’s just a lot more work. You will not find anything about SFTP in this posting, maybe I will talk about that later (but see here an example)
FTPS stands for FTP Secure, RFC 4217 – Securing FTP with TLS. And it’s secured with the TLS protocol. Oracle has added support for this protocol to utl_tcp in version 11.2.0.2, UTP_TCP.
As with https you need a certificate (chain) of the server in a Oracle Wallet.
You have pass the path of this wallet to the utl_tcp.open_connection procedure

1
2
3
4
5
t_conn := utl_tcp.open_connection( p_host
, p_port
, wallet_path => p_wallet_path
, wallet_password => p_wallet_password
); -- open connection

And you tell the server that you want to secure the connection

1
write_cmd( t_conn, 'AUTH', 'TLS' )

If the server is OK with that, do it.

1
utl_tcp.secure_connection( t_conn );

That’s enough to secure the control channel of the FTP connection!

Doing the same for the data channel requires just two more commands

1
2
write_cmd( t_conn, 'PBSZ', '0' )
write_cmd( t_conn, 'PROT', 'P' )

A complete example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
declare
  t_conn utl_tcp.connection;
  t_blob blob;
  t_clob clob;
  t_reply number;
  type tp_dir_listing is table of varchar2(32767) index by pls_integer;
  t_dir_listing tp_dir_listing;
--
  g_response varchar2(1000 char);
  g_wallet_path     varchar2(1000 char);
  g_wallet_password varchar2(1000 char);
--
  function read_reply( p_conn in out nocopy utl_tcp.connection, p_verbose boolean := true )
  return number
  is
    t_line varchar(32767);
    t_code varchar2(3 char);
  begin
    t_line := utl_tcp.get_line( p_conn, true );
    if nvl( length( t_line ), 0 )  0
       and instr( t_tmp, '(', 1, 2 ) = 0
       and instr( t_tmp, ')' ) > 0
       and instr( t_tmp, ')', 1, 2 ) = 0
       )
    then
      t_tmp := substr( t_tmp, instr( t_tmp, '(' ) + 1 );
      t_tmp := substr( t_tmp, 1, instr( t_tmp, ')' ) - 1 );
    else
      t_tmp := translate( t_tmp, '1234567890,' || t_tmp, '1234567890,' );
    end if;
    t_host := substr( t_tmp, 1, instr( t_tmp, ',', 1, 4 ) - 1 );
    t_host := replace( t_host, ',', '.' );
    t_tmp := substr( t_tmp, instr( t_tmp, ',', 1, 4 ) + 1 );
    t_port := to_number( substr( t_tmp, 1, instr( t_tmp, ',' ) - 1 ) );
    t_port := t_port * 256;
    t_port := t_port + to_number( substr( t_tmp, instr( t_tmp, ',' ) + 1 ) );
    t_conn := utl_tcp.open_connection( t_host, t_port, wallet_path => g_wallet_path, wallet_password => g_wallet_password );
    return t_conn;
  end;
--
  function write_cmd( p_conn in out nocopy utl_tcp.connection, p_cmd varchar2, p_param varchar2 := '' )
  return number
  is
    t_dummy pls_integer;
  begin
    t_dummy := utl_tcp.write_line( p_conn, p_cmd || rtrim( ' ' || p_param ) );
    return read_reply( p_conn );
  end;
--
  function open_data_channel( p_conn in out nocopy utl_tcp.connection )
  return utl_tcp.connection
  is
    t_tmp varchar2(32767);
    t_host varchar2(1000 char);
    t_port pls_integer;
    t_conn utl_tcp.connection;
  begin
    if write_cmd( p_conn, 'PASV' ) != 227
    then
      raise_application_error( -20001, 'Could not set PASSIVE: ' || g_response, true );
    end if;
    t_tmp := g_response;
    if (   instr( t_tmp, '(' ) > 0
       and instr( t_tmp, '(', 1, 2 ) = 0
       and instr( t_tmp, ')' ) > 0
       and instr( t_tmp, ')', 1, 2 ) = 0
       )
    then
      t_tmp := substr( t_tmp, instr( t_tmp, '(' ) + 1 );
      t_tmp := substr( t_tmp, 1, instr( t_tmp, ')' ) - 1 );
    else
      t_tmp := translate( t_tmp, '1234567890,' || t_tmp, '1234567890,' );
    end if;
    t_host := substr( t_tmp, 1, instr( t_tmp, ',', 1, 4 ) - 1 );
    t_host := replace( t_host, ',', '.' );
    t_tmp := substr( t_tmp, instr( t_tmp, ',', 1, 4 ) + 1 );
    t_port := to_number( substr( t_tmp, 1, instr( t_tmp, ',' ) - 1 ) );
    t_port := t_port * 256;
    t_port := t_port + to_number( substr( t_tmp, instr( t_tmp, ',' ) + 1 ) );
    t_conn := utl_tcp.open_connection( t_host, t_port, wallet_path => g_wallet_path, wallet_password => g_wallet_password );
    return t_conn;
  end;
--
  function login
    ( p_host varchar2
    , p_usr  varchar2 := null
    , p_pw   varchar2 := null
    , p_port number := 21
    , p_account varchar2 := null
    , p_wallet_path varchar2 := null
    , p_wallet_password varchar2 := null
    )
  return utl_tcp.connection
  is
    t_reply number;
    t_conn utl_tcp.connection;
    t_usr varchar2(1000 char);
    t_pw  varchar2(1000 char);
    t_acc varchar2(1000 char);
  begin
    t_conn := utl_tcp.open_connection( p_host, p_port, wallet_path => p_wallet_path, wallet_password => p_wallet_password ); -- open connection
    case read_reply( t_conn )
      when 120
      then
        raise_application_error( -20001, 'FTP server not ready: ' || g_response, true );
      when 421
      then
        raise_application_error( -20001, 'FTP server not available: ' || g_response, true );
      else
        null;
    end case;
    if write_cmd( t_conn, 'AUTH', 'TLS' ) = 234
    then
      utl_tcp.secure_connection( t_conn );
      if     write_cmd( t_conn, 'PBSZ', '0' ) = 200
         and write_cmd( t_conn, 'PROT', 'P' ) = 200
      then
        g_wallet_path     := p_wallet_path;
        g_wallet_password := p_wallet_password;
      end if;
    end if;
    if p_usr is null
    then
      t_usr := 'anonymous';
      t_pw  := 'anonymous@mysite.com';
      t_acc := '*********';
    else
      t_usr := p_usr;
      t_pw  := p_pw;
      t_acc := p_account;
    end if;
    t_reply := write_cmd( t_conn, 'USER', t_usr );
    if t_reply = 331
    then
      t_reply := write_cmd( t_conn, 'PASS', t_pw );
    end if;
    if t_reply = 332
    then
      t_reply := write_cmd( t_conn, 'ACCT', t_acc );
    end if;
    if t_reply not in ( 230, 202 )
    then
      raise_application_error( -20001, 'Could not log in: ' || g_response, true );
    end if;
    return t_conn;
  end;
--
  procedure get_file
    ( p_conn in out nocopy utl_tcp.connection
    , p_path in varchar2
    , p_file in out blob
    )
  is
    t_reply number;
    t_buf raw(32767);
    t_cnt pls_integer;
    t_conn utl_tcp.connection;
  begin
    t_reply := write_cmd( p_conn, 'TYPE', 'I' );
    t_conn := open_data_channel( p_conn );
    t_reply := write_cmd( p_conn, 'RETR', p_path );
    if t_reply in ( 125, 150 )
    then
      if g_wallet_path is not null
      then
        utl_tcp.secure_connection( t_conn );
      end if;
      dbms_lob.createtemporary( p_file, true );
      begin
        loop
          t_cnt := utl_tcp.read_raw( t_conn, t_buf, 32767 );
          dbms_lob.writeappend( p_file, t_cnt, t_buf );
        end loop;
      exception
        when utl_tcp.end_of_input
        then
          null;
      end;
      utl_tcp.close_connection( t_conn );
      t_reply := read_reply( p_conn );
    end if;
    if t_reply not in ( 226, 250 )
    then
      raise_application_error( -20001, 'Could not retrieve file: ' || g_response, true );
    end if;
  end;
--
  procedure get_file
    ( p_conn in out nocopy utl_tcp.connection
    , p_path in varchar2
    , p_file in out clob
    )
  is
    t_reply number;
    t_buf varchar2(32767);
    t_cnt pls_integer;
    t_conn utl_tcp.connection;
  begin
    t_reply := write_cmd( p_conn, 'TYPE', 'A' );
    t_conn := open_data_channel( p_conn );
    t_reply := write_cmd( p_conn, 'RETR', p_path );
    if t_reply in ( 125, 150 )
    then
      if g_wallet_path is not null
      then
        utl_tcp.secure_connection( t_conn );
      end if;
      dbms_lob.createtemporary( p_file, true );
      begin
        loop
          t_buf := utl_i18n.raw_to_char( utl_tcp.get_raw( t_conn, 32767 ), 'US7ASCII' );
          dbms_lob.writeappend( p_file, length( t_buf ), t_buf );
        end loop;
      exception
        when utl_tcp.end_of_input
        then
          null;
      end;
      utl_tcp.close_connection( t_conn );
      t_reply := read_reply( p_conn );
    end if;
    if t_reply not in ( 226, 250 )
    then
      raise_application_error( -20001, 'Could not retrieve file: ' || g_response, true );
    end if;
  end;
--
  procedure nlst
    ( p_conn in out nocopy utl_tcp.connection
    , p_dir_listing in out tp_dir_listing
    , p_path in varchar2 := null
    )
  is
    t_reply number;
    t_conn utl_tcp.connection;
  begin
    p_dir_listing.delete;
    t_reply := write_cmd( p_conn, 'TYPE', 'A' );
    t_conn := open_data_channel( p_conn );
    t_reply := write_cmd( p_conn, 'NLST', p_path );
    if t_reply in ( 125, 150 )
    then
      if g_wallet_path is not null
      then
        utl_tcp.secure_connection( t_conn );
      end if;
      begin
        loop
          p_dir_listing( p_dir_listing.count + 1 ) := utl_tcp.get_line( t_conn, true );
        end loop;
      exception
        when utl_tcp.end_of_input
        then
          null;
      end;
      utl_tcp.close_connection( t_conn );
      t_reply := read_reply( p_conn );
    end if;
    if t_reply != 226
    then
      raise_application_error( -20001, 'Could not get NLST: ' || g_response, true );
    end if;
  end;
--
begin
  t_conn := login( 'localhost', p_wallet_path => 'file:C:\oracle\wallet', p_wallet_password => 'amis.rules.again' );
--  get_file( t_conn, '/my_dir/my_ascii_file.txt', t_clob );
  nlst( t_conn, t_dir_listing, '*.txt' );
  t_reply := write_cmd( t_conn, 'QUIT' );
  utl_tcp.close_connection( t_conn );
end;

You will find the same example here

Tags:,

15 Comments

  1. Trung Vu Duc March 12, 2018
  2. Jennifer Shi October 6, 2016
    • Anton Scheffer October 6, 2016
  3. Aswin Kumar April 21, 2016
    • Anton Scheffer April 21, 2016
  4. Marianne Mulinski April 2, 2016
  5. Johny HP February 26, 2016
  6. Johny HP February 26, 2016
    • Anton Scheffer February 26, 2016
      • Johny HP March 31, 2016
  7. Mike Huitson February 5, 2016
  8. Mike Huitson February 5, 2016
    • Anton Scheffer February 5, 2016
  9. Francisco Javier Melado March 10, 2015