yaz-client: handle HTTP redirects
[yaz-moved-to-github.git] / client / client.c
index 2692a9c..45eb69d 100644 (file)
@@ -1,5 +1,5 @@
 /* This file is part of the YAZ toolkit.
- * Copyright (C) 1995-2009 Index Data
+ * Copyright (C) 1995-2010 Index Data
  * See the file LICENSE for details.
  */
 /** \file client.c
@@ -141,7 +141,7 @@ static int scan_stepSize = 0;
 static int scan_position = 1;
 static int scan_size = 20;
 static char cur_host[200];
-static int last_hit_count = 0;
+static Odr_int last_hit_count = 0;
 
 typedef enum {
     QueryType_Prefix,
@@ -182,6 +182,7 @@ static void close_session(void);
 static void marc_file_write(const char *buf, size_t sz);
 
 static void wait_and_handle_response(int one_response_only);
+static Z_GDU *get_HTTP_Request_url(ODR odr, const char *url);
 
 ODR getODROutputStream(void)
 {
@@ -286,14 +287,14 @@ int send_apdu(Z_APDU *a)
     return 1;
 }
 
-static void print_stringn(const unsigned char *buf, size_t len)
+static void print_stringn(const char *buf, size_t len)
 {
     size_t i;
-    for (i = 0; i<len; i++)
+    for (i = 0; i < len; i++)
         if ((buf[i] <= 126 && buf[i] >= 32) || strchr("\n\r\t\f", buf[i]))
             printf("%c", buf[i]);
         else
-            printf("\\X%02X", buf[i]);
+            printf("\\X%02X", ((const unsigned char *)buf)[i]);
 }
 
 static void print_refid(Z_ReferenceId *id)
@@ -301,7 +302,7 @@ static void print_refid(Z_ReferenceId *id)
     if (id)
     {
         printf("Reference Id: ");
-        print_stringn(id->buf, id->len);
+        print_stringn((const char *) id->buf, id->len);
         printf("\n");
     }
 }
@@ -706,10 +707,6 @@ int session_connect(const char *arg)
     }
 #endif
     protocol = conn->protocol;
-    if (conn->protocol == PROTO_HTTP)
-        set_base("");
-    else
-        set_base("Default");
     printf("Connecting...");
     fflush(stdout);
     if (cs_connect(conn, add) < 0)
@@ -722,7 +719,10 @@ int session_connect(const char *arg)
     printf("OK.\n");
     cs_print_session_info(conn);
     if (basep && *basep)
-        set_base (basep);
+        set_base(basep);
+    else if (protocol == PROTO_Z3950)
+        set_base("Default");
+
     if (protocol == PROTO_Z3950)
     {
         send_initRequest(type_and_host);
@@ -739,9 +739,13 @@ int cmd_open(const char *arg)
         strncpy(cur_host, arg, sizeof(cur_host)-1);
         cur_host[sizeof(cur_host)-1] = 0;
     }
+
+    set_base("");
     r = session_connect(cur_host);
     if (conn && conn->protocol == PROTO_HTTP)
         queryType = QueryType_CQL;
+
+
     return r;
 }
 
@@ -810,7 +814,7 @@ int cmd_authentication(const char *arg)
 /* SEARCH SERVICE ------------------------------ */
 static void display_record(Z_External *r);
 
-static void print_record(const unsigned char *buf, size_t len)
+static void print_record(const char *buf, size_t len)
 {
     size_t i = len;
     print_stringn(buf, len);
@@ -870,91 +874,72 @@ static void display_record(Z_External *r)
             }
         }
     }
-    if (oid && !oid_oidcmp(oid, yaz_oid_recsyn_soif))
-    {
-        print_record((const unsigned char *) r->u.octet_aligned->buf,
-                     r->u.octet_aligned->len);
-        marc_file_write((const char *) r->u.octet_aligned->buf,
-                        r->u.octet_aligned->len);
-    }
-    else if (oid && r->which == Z_External_octet)
+    if (oid && r->which == Z_External_octet)
     {
-        const char *octet_buf = (char*)r->u.octet_aligned->buf;
-        if (oid && (!oid_oidcmp(oid, yaz_oid_recsyn_xml)
-                    || !oid_oidcmp(oid, yaz_oid_recsyn_xml)
-                    || !oid_oidcmp(oid, yaz_oid_recsyn_html)))
+        const char *octet_buf = (const char*)r->u.octet_aligned->buf;
+        size_t octet_len = r->u.octet_aligned->len;
+        if (!oid_oidcmp(oid, yaz_oid_recsyn_xml)
+            || !oid_oidcmp(oid, yaz_oid_recsyn_xml)
+            || !oid_oidcmp(oid, yaz_oid_recsyn_html))
         {
-            print_record((const unsigned char *) octet_buf,
-                         r->u.octet_aligned->len);
+            fwrite(octet_buf, 1, octet_len, stdout);
         }
-        else if (oid && !oid_oidcmp(oid, yaz_oid_recsyn_postscript))
-        {
-            int size = r->u.octet_aligned->len;
-            if (size > 100)
-                size = 100;
-            print_record((const unsigned char *) octet_buf, size);
-        }
-        else
+        else if (yaz_oid_is_iso2709(oid))
         {
-            if (oid && yaz_oid_is_iso2709(oid))
+            const char *result;
+            size_t rlen;
+            yaz_iconv_t cd = 0;
+            yaz_marc_t mt = yaz_marc_create();
+            const char *from = 0;
+            
+            if (marcCharset && !strcmp(marcCharset, "auto"))
             {
-                const char *result;
-                size_t rlen;
-                yaz_iconv_t cd = 0;
-                yaz_marc_t mt = yaz_marc_create();
-                const char *from = 0;
-
-                if (marcCharset && !strcmp(marcCharset, "auto"))
-                {
-                    if (!oid_oidcmp(oid, yaz_oid_recsyn_usmarc))
-                    {
-                        if (octet_buf[9] == 'a')
-                            from = "UTF-8";
-                        else
-                            from = "MARC-8";
-                    }
-                    else
-                        from = "ISO-8859-1";
-                }
-                else if (marcCharset)
-                    from = marcCharset;
-                if (outputCharset && from)
+                if (!oid_oidcmp(oid, yaz_oid_recsyn_usmarc))
                 {
-                    cd = yaz_iconv_open(outputCharset, from);
-                    printf("convert from %s to %s", from,
-                            outputCharset);
-                    if (!cd)
-                        printf(" unsupported\n");
+                    if (octet_buf[9] == 'a')
+                        from = "UTF-8";
                     else
-                    {
-                        yaz_marc_iconv(mt, cd);
-                        printf("\n");
-                    }
+                        from = "MARC-8";
                 }
-
-                if (yaz_marc_decode_buf(mt, octet_buf, r->u.octet_aligned->len,
-                                        &result, &rlen)> 0)
+                else
+                    from = "ISO-8859-1";
+            }
+            else if (marcCharset)
+                from = marcCharset;
+            if (outputCharset && from)
+            {
+                cd = yaz_iconv_open(outputCharset, from);
+                printf("convert from %s to %s", from,
+                       outputCharset);
+                if (!cd)
+                    printf(" unsupported\n");
+                else
                 {
-                    if (fwrite(result, rlen, 1, stdout) != 1)
-                    {
-                        printf("write to stdout failed\n");
-                    }
+                    yaz_marc_iconv(mt, cd);
+                    printf("\n");
                 }
-                else
+            }
+            
+            if (yaz_marc_decode_buf(mt, octet_buf, octet_len,
+                                    &result, &rlen)> 0)
+            {
+                if (fwrite(result, rlen, 1, stdout) != 1)
                 {
-                    printf("bad MARC. Dumping as it is:\n");
-                    print_record((const unsigned char*) octet_buf,
-                                  r->u.octet_aligned->len);
+                    printf("write to stdout failed\n");
                 }
-                yaz_marc_destroy(mt);
-                if (cd)
-                    yaz_iconv_close(cd);
             }
             else
             {
-                print_record((const unsigned char*) octet_buf,
-                             r->u.octet_aligned->len);
+                printf("bad MARC. Dumping as it is:\n");
+                print_record(octet_buf, octet_len);
             }
+            yaz_marc_destroy(mt);
+            if (cd)
+                yaz_iconv_close(cd);
+        }
+        else
+        {
+            print_record(octet_buf, octet_len);
         }
         marc_file_write(octet_buf, r->u.octet_aligned->len);
     }
@@ -965,7 +950,7 @@ static void display_record(Z_External *r)
             printf("Expecting single SUTRS type for SUTRS.\n");
             return;
         }
-        print_record(r->u.sutrs->buf, r->u.sutrs->len);
+        print_record((const char *) r->u.sutrs->buf, r->u.sutrs->len);
         marc_file_write((const char *) r->u.sutrs->buf, r->u.sutrs->len);
     }
     else if (oid && !oid_oidcmp(oid, yaz_oid_recsyn_grs_1))
@@ -1207,32 +1192,8 @@ static int send_deleteResultSetRequest(const char *arg)
 }
 
 #if YAZ_HAVE_XML2
-static int send_srw(Z_SRW_PDU *sr)
+static int send_gdu(Z_GDU *gdu)
 {
-    const char *charset = negotiationCharset;
-    const char *host_port = cur_host;
-    Z_GDU *gdu;
-    char *path = 0;
-
-    path = (char *) odr_malloc(out, 2+strlen(databaseNames[0]));
-    *path = '/';
-    strcpy(path+1, databaseNames[0]);
-
-    gdu = z_get_HTTP_Request_host_path(out, host_port, path);
-
-    if (!yaz_matchstr(sru_method, "get"))
-    {
-        yaz_sru_get_encode(gdu->u.HTTP_Request, sr, out, charset);
-    }
-    else if (!yaz_matchstr(sru_method, "post"))
-    {
-        yaz_sru_post_encode(gdu->u.HTTP_Request, sr, out, charset);
-    }
-    else if (!yaz_matchstr(sru_method, "soap"))
-    {
-        yaz_sru_soap_encode(gdu->u.HTTP_Request, sr, out, charset);
-    }
-
     if (z_GDU(out, &gdu, 0, 0))
     {
         /* encode OK */
@@ -1258,6 +1219,100 @@ static int send_srw(Z_SRW_PDU *sr)
     }
     return 0;
 }
+
+static int send_srw_host_path(Z_SRW_PDU *sr, const char *host_port,
+                              char *path)
+{
+    const char *charset = negotiationCharset;
+    Z_GDU *gdu;
+
+    gdu = z_get_HTTP_Request_host_path(out, host_port, path);
+
+    if (auth)
+    {
+        if (auth->which == Z_IdAuthentication_open)
+        {
+            char **darray;
+            int num;
+            nmem_strsplit(out->mem, "/", auth->u.open, &darray, &num);
+            if (num >= 1)
+                sr->username = darray[0];
+            if (num >= 2)
+                sr->password = darray[1];
+        }
+        else if (auth->which == Z_IdAuthentication_idPass)
+        {
+            sr->username = auth->u.idPass->userId;
+            sr->password = auth->u.idPass->password;
+        }
+    }
+
+    if (!yaz_matchstr(sru_method, "get"))
+    {
+        yaz_sru_get_encode(gdu->u.HTTP_Request, sr, out, charset);
+    }
+    else if (!yaz_matchstr(sru_method, "post"))
+    {
+        yaz_sru_post_encode(gdu->u.HTTP_Request, sr, out, charset);
+    }
+    else if (!yaz_matchstr(sru_method, "soap"))
+    {
+        yaz_sru_soap_encode(gdu->u.HTTP_Request, sr, out, charset);
+    }
+    return send_gdu(gdu);
+}
+
+static int send_srw(Z_SRW_PDU *sr)
+{
+    char *path = yaz_encode_sru_dbpath_odr(out, databaseNames[0]);
+    return send_srw_host_path(sr, cur_host, path);
+}
+
+static int send_SRW_redirect(const char *uri, Z_HTTP_Response *cookie_hres)
+{
+    const char *username = 0;
+    const char *password = 0;
+    struct Z_HTTP_Header *h;
+    Z_GDU *gdu = get_HTTP_Request_url(out, uri);
+
+    gdu->u.HTTP_Request->method = odr_strdup(out, "GET");
+    z_HTTP_header_add(out, &gdu->u.HTTP_Request->headers, "Accept",
+                      "text/xml");
+    
+    for (h = cookie_hres->headers; h; h = h->next)
+    {
+        if (!strcmp(h->name, "Set-Cookie"))
+            z_HTTP_header_add(out, &gdu->u.HTTP_Request->headers,
+                              "Cookie", h->value);
+    }
+
+    if (auth)
+    {
+        if (auth->which == Z_IdAuthentication_open)
+        {
+            char **darray;
+            int num;
+            nmem_strsplit(out->mem, "/", auth->u.open, &darray, &num);
+            if (num >= 1)
+                username = darray[0];
+            if (num >= 2)
+                password = darray[1];
+        }
+        else if (auth->which == Z_IdAuthentication_idPass)
+        {
+            username = auth->u.idPass->userId;
+            password = auth->u.idPass->password;
+        }
+    }
+
+    if (username && password)
+    {
+        z_HTTP_header_add_basic_auth(out, &gdu->u.HTTP_Request->headers,
+                                     username, password);
+    }
+
+    return send_gdu(gdu);
+}
 #endif
 
 #if YAZ_HAVE_XML2
@@ -1845,7 +1900,7 @@ void process_ESResponse(Z_ExtendedServicesResponse *res)
         if (id)
         {
             printf("Target Reference: ");
-            print_stringn (id->buf, id->len);
+            print_stringn((const char *) id->buf, id->len);
             printf("\n");
         }
         if (ext->which == Z_External_update)
@@ -1965,7 +2020,7 @@ static Z_External *create_external_itemRequest(void)
     return r;
 }
 
-static Z_External *create_external_ILL_APDU(int which)
+static Z_External *create_external_ILL_APDU(void)
 {
     struct ill_get_ctl ctl;
     ILL_APDU *ill_apdu;
@@ -2067,7 +2122,7 @@ static Z_External *create_ItemOrderExternal(const char *type, int itemno,
     {
         printf("using ILL-request\n");
         r->u.itemOrder->u.esRequest->notToKeep->itemRequest =
-            create_external_ILL_APDU(ILL_APDU_ILL_Request);
+            create_external_ILL_APDU();
     }
     else if (!strcmp(type, "xml") || !strcmp(type, "3"))
     {
@@ -2513,6 +2568,130 @@ static int cmd_init(const char *arg)
     return 2;
 }
 
+static Z_GDU *get_HTTP_Request_url(ODR odr, const char *url)
+{
+    Z_GDU *p = z_get_HTTP_Request(odr);
+    const char *host = url;
+    const char *cp0 = strstr(host, "://");
+    const char *cp1 = 0;
+    if (cp0)
+        cp0 = cp0+3;
+    else
+        cp0 = host;
+    
+    cp1 = strchr(cp0, '/');
+    if (!cp1)
+        cp1 = cp0 + strlen(cp0);
+    
+    if (cp0 && cp1)
+    {
+        char *h = (char*) odr_malloc(odr, cp1 - cp0 + 1);
+        memcpy (h, cp0, cp1 - cp0);
+        h[cp1-cp0] = '\0';
+        z_HTTP_header_add(odr, &p->u.HTTP_Request->headers, "Host", h);
+    }
+    p->u.HTTP_Request->path = odr_strdup(odr, *cp1 ? cp1 : "/");
+    return p;
+}
+
+static WRBUF get_url(const char *uri, WRBUF username, WRBUF password,
+                     int *code, int show_headers)
+{
+    WRBUF result = 0;
+    ODR out = odr_createmem(ODR_ENCODE);
+    ODR in = odr_createmem(ODR_DECODE);
+    Z_GDU *gdu = get_HTTP_Request_url(out, uri);
+    
+    gdu->u.HTTP_Request->method = odr_strdup(out, "GET");
+    if (username && password)
+    {
+        z_HTTP_header_add_basic_auth(out, &gdu->u.HTTP_Request->headers,
+                                     wrbuf_cstr(username),
+                                     wrbuf_cstr(password));
+    }
+    z_HTTP_header_add(out, &gdu->u.HTTP_Request->headers, "Accept",
+                      "text/xml");
+    if (!z_GDU(out, &gdu, 0, 0))
+    {
+        yaz_log(YLOG_WARN, "Can not encode HTTP request URL:%s", uri);        
+    }
+    else
+    {
+        void *add;
+        COMSTACK conn = cs_create_host(uri, 1, &add);
+        if (cs_connect(conn, add) < 0)
+            yaz_log(YLOG_WARN, "Can not connect to URL:%s", uri);
+        else
+        {
+            int len;
+            char *buf = odr_getbuf(out, &len, 0);
+            
+            if (cs_put(conn, buf, len) < 0)
+                yaz_log(YLOG_WARN, "cs_put failed URL:%s", uri);
+            else
+            {
+                char *netbuffer = 0;
+                int netlen = 0;
+                int res = cs_get(conn, &netbuffer, &netlen);
+                if (res <= 0)
+                {
+                    yaz_log(YLOG_WARN, "cs_get failed URL:%s", uri);
+                }
+                else
+                {
+                    Z_GDU *gdu;
+                    odr_setbuf(in, netbuffer, res, 0);
+                    if (!z_GDU(in, &gdu, 0, 0)
+                        || gdu->which != Z_GDU_HTTP_Response)
+                    {
+                        yaz_log(YLOG_WARN, "decode failed URL: %s", uri);
+                    }
+                    else
+                    {
+                        Z_HTTP_Response *res = gdu->u.HTTP_Response;
+                        struct Z_HTTP_Header *h;
+                        result = wrbuf_alloc();
+                        if (show_headers)
+                        {
+                            
+                            wrbuf_printf(result, "HTTP %d\n", res->code);
+                            for (h = res->headers; h; h = h->next)
+                                wrbuf_printf(result, "%s: %s\n",
+                                             h->name, h->value);
+                        }
+                        *code = res->code;
+                        wrbuf_write(result, res->content_buf, res->content_len);
+                    }
+                }
+                xfree(netbuffer);
+            }
+            cs_close(conn);
+        }
+    }
+    odr_destroy(out);
+    odr_destroy(in);
+    return result;
+}
+    
+
+static int cmd_url(const char *arg)
+{
+    int code = 0;
+    WRBUF res = get_url(arg, 0, 0, &code, 1);
+    if (res)
+    {
+        if (wrbuf_len(res) > 1200)
+        {
+            fwrite(wrbuf_buf(res), 1, 1200, stdout);
+            printf(".. out of %lld\n", (long long) wrbuf_len(res));
+        }
+        else
+            puts(wrbuf_cstr(res));
+        wrbuf_destroy(res);
+    }
+    return 0;
+}
+
 static int cmd_sru(const char *arg)
 {
     if (!*arg)
@@ -2660,36 +2839,50 @@ static int cmd_setnames(const char *arg)
 
 /* PRESENT SERVICE ----------------------------- */
 
-static void parse_show_args(const char *arg_c, char *setstring,
-                            Odr_int *start, Odr_int *number)
+static int parse_show_args(const char *arg_c, char *setstring,
+                           Odr_int *start, Odr_int *number)
 {
-    char arg[40];
-    char *p;
+    char *end_ptr;
 
-    strncpy(arg, arg_c, sizeof(arg)-1);
-    arg[sizeof(arg)-1] = '\0';
+    if (setnumber >= 0)
+        sprintf(setstring, "%d", setnumber);
+    else
+        *setstring = '\0';
 
-    if ((p = strchr(arg, '+')))
+    if (!strcmp(arg_c, "all"))
     {
-        *number = atoi(p + 1);
-        *p = '\0';
+        *number = last_hit_count;
+        *start = 1;
     }
-    if (*arg)
+    *start = odr_strtol(arg_c, &end_ptr, 10);
+    if (end_ptr == arg_c || *end_ptr == '\0')
+        return 1;
+    while (isspace(*(unsigned char *)end_ptr))
+        end_ptr++;
+    if (*end_ptr != '+')
     {
-        if (!strcmp(arg, "all"))
-        {
-            *number = last_hit_count;
-            *start = 1;
-        }
-        else
-            *start = atoi(arg);
+        printf("Bad show arg: expected +. Got %s\n", end_ptr);
+        return 0;
     }
-    if (p && (p=strchr(p+1, '+')))
-        strcpy(setstring, p+1);
-    else if (setnumber >= 0)
-        sprintf(setstring, "%d", setnumber);
-    else
-        *setstring = '\0';
+    end_ptr++;
+    arg_c = end_ptr;
+    *number = odr_strtol(arg_c, &end_ptr, 10);
+    if (end_ptr == arg_c)
+    {
+        printf("Bad show arg: expected number after +\n");
+        return 0;
+    }
+    if (*end_ptr == '\0')
+        return 1;
+    while (isspace(*(unsigned char *)end_ptr))
+        end_ptr++;
+    if (*end_ptr != '+')
+    {
+        printf("Bad show arg: + expected. Got %s\n", end_ptr);
+        return 0;
+    }
+    strcpy(setstring, end_ptr+1);
+    return 1;
 }
 
 static int send_presentRequest(const char *arg)
@@ -2702,7 +2895,8 @@ static int send_presentRequest(const char *arg)
 
     req->referenceId = set_refid(out);
 
-    parse_show_args(arg, setstring, &setno, &nos);
+    if (!parse_show_args(arg, setstring, &setno, &nos))
+        return 0;
     if (*setstring)
         req->resultSetId = setstring;
 
@@ -2793,7 +2987,8 @@ static int send_SRW_presentRequest(const char *arg)
 
     if (!sr)
         return 0;
-    parse_show_args(arg, setstring, &setno, &nos);
+    if (!parse_show_args(arg, setstring, &setno, &nos))
+        return 0;
     sr->u.request->startRecord = odr_intdup(out, setno);
     sr->u.request->maximumRecords = odr_intdup(out, nos);
     if (record_schema)
@@ -4009,6 +4204,12 @@ static void http_response(Z_HTTP_Response *hres)
     int ret = -1;
     const char *connection_head = z_HTTP_header_lookup(hres->headers,
                                                        "Connection");
+
+    if (hres->code != 200)
+    {
+        printf("HTTP Error Status=%d\n", hres->code);
+    }
+
     if (!yaz_srw_check_content_type(hres))
         printf("Content type does not appear to be XML\n");
     else
@@ -4036,12 +4237,14 @@ static void http_response(Z_HTTP_Response *hres)
                 printf("Got update response. Status: %s\n",
                        sr->u.update_response->operationStatus);
             else
+            {
+                printf("Decoding of SRW package failed\n");
                 ret = -1;
+            }
         }
         else if (soap_package && (soap_package->which == Z_SOAP_fault
                                   || soap_package->which == Z_SOAP_error))
         {
-            printf("HTTP Error Status=%d\n", hres->code);
             printf("SOAP Fault code %s\n",
                     soap_package->u.fault->fault_code);
             printf("SOAP Fault string %s\n",
@@ -4058,17 +4261,7 @@ static void http_response(Z_HTTP_Response *hres)
         odr_destroy(o);
     }
     if (ret)
-    {
-        if (hres->code != 200)
-        {
-            printf("HTTP Error Status=%d\n", hres->code);
-        }
-        else
-        {
-            printf("Decoding of SRW package failed\n");
-        }
-        close_session();
-    }
+        close_session(); /* close session on error */
     else
     {
         if (!strcmp(hres->version, "1.0"))
@@ -4087,9 +4280,12 @@ static void http_response(Z_HTTP_Response *hres)
 }
 #endif
 
+#define max_HTTP_redirects 2
+
 static void wait_and_handle_response(int one_response_only)
 {
     int reconnect_ok = 1;
+    int no_redirects = 0;
     int res;
     char *netbuffer= 0;
     int netbufferlen = 0;
@@ -4218,7 +4414,25 @@ static void wait_and_handle_response(int one_response_only)
 #if YAZ_HAVE_XML2
         else if (gdu->which == Z_GDU_HTTP_Response)
         {
-            http_response(gdu->u.HTTP_Response);
+            Z_HTTP_Response *hres = gdu->u.HTTP_Response;
+            int code = hres->code;
+            const char *location = 0;
+            if ((code == 301 || code == 302)
+                && no_redirects < max_HTTP_redirects
+                && !yaz_matchstr(sru_method, "get")
+                && (location = z_HTTP_header_lookup(hres->headers, "Location")))
+            {
+                session_connect(location);
+                no_redirects++;
+                if (conn)
+                {
+                    if (send_SRW_redirect(location, hres) == 2)
+                        continue;
+                }
+                printf("Redirect failed\n");
+            }
+            else
+                http_response(gdu->u.HTTP_Response);
         }
 #endif
         if (one_response_only)
@@ -4581,6 +4795,7 @@ static struct {
     {"help", cmd_help, "", NULL,0,NULL},
     {"init", cmd_init, "", NULL,0,NULL},
     {"sru", cmd_sru, "<method> <version>", NULL,0,NULL},
+    {"url", cmd_url, "<url>", NULL,0,NULL},
     {"exit", cmd_quit, "",NULL,0,NULL},
     {0,0,0,0,0,0}
 };
@@ -4862,7 +5077,10 @@ static void client(void)
         {
             line_in=readline(C_PROMPT);
             if (!line_in)
+            {
+                putchar('\n');
                 break;
+            }
 #if HAVE_READLINE_HISTORY_H
             if (*line_in)
                 add_history(line_in);