MSM_IPC Router exports socket interface to the user-space processes
to communicate with the processes in the remote processors

Change-Id: I2d4039dd1825117c125d6975b22aa7f144659643
Signed-off-by: Karthikeyan Ramasubramanian <[email protected]>
---
 include/linux/msm_ipc.h      |   47 ++++
 include/linux/socket.h       |    5 +-
 net/msm_ipc/Makefile         |    2 +-
 net/msm_ipc/msm_ipc_router.c |    7 +-
 net/msm_ipc/msm_ipc_router.h |    8 +
 net/msm_ipc/msm_ipc_socket.c |  520 ++++++++++++++++++++++++++++++++++++++++++
 6 files changed, 585 insertions(+), 4 deletions(-)
 create mode 100644 net/msm_ipc/msm_ipc_socket.c

diff --git a/include/linux/msm_ipc.h b/include/linux/msm_ipc.h
index 47d678c..2db5d6f 100644
--- a/include/linux/msm_ipc.h
+++ b/include/linux/msm_ipc.h
@@ -24,4 +24,51 @@ struct msm_ipc_addr {
        } addr;
 };
 
+#define MSM_IPC_WAIT_FOREVER   (~0)
+
+/*
+ * Socket API
+ */
+
+#ifndef AF_MSM_IPC
+#define AF_MSM_IPC             39
+#endif
+
+#ifndef PF_MSM_IPC
+#define PF_MSM_IPC             AF_MSM_IPC
+#endif
+
+#define MSM_IPC_ADDR_NAME              1
+#define MSM_IPC_ADDR_ID                        2
+
+struct sockaddr_msm_ipc {
+       unsigned short family;
+       struct msm_ipc_addr address;
+       unsigned char reserved;
+};
+
+#define IPC_ROUTER_IOCTL_MAGIC (0xC3)
+
+#define IPC_ROUTER_IOCTL_GET_VERSION \
+       _IOR(IPC_ROUTER_IOCTL_MAGIC, 0, unsigned int)
+
+#define IPC_ROUTER_IOCTL_GET_MTU \
+       _IOR(IPC_ROUTER_IOCTL_MAGIC, 1, unsigned int)
+
+#define IPC_ROUTER_IOCTL_LOOKUP_SERVER \
+       _IOWR(IPC_ROUTER_IOCTL_MAGIC, 2, struct sockaddr_msm_ipc)
+
+#define IPC_ROUTER_IOCTL_GET_CURR_PKT_SIZE \
+       _IOR(IPC_ROUTER_IOCTL_MAGIC, 3, unsigned int)
+
+#define IPC_ROUTER_IOCTL_BIND_CONTROL_PORT \
+       _IOR(IPC_ROUTER_IOCTL_MAGIC, 4, unsigned int)
+
+struct server_lookup_args {
+       struct msm_ipc_port_name port_name;
+       int num_entries_in_array;
+       int num_entries_found;
+       struct msm_ipc_port_addr port_addr[0];
+};
+
 #endif
diff --git a/include/linux/socket.h b/include/linux/socket.h
index d2b5e98..a23198e 100644
--- a/include/linux/socket.h
+++ b/include/linux/socket.h
@@ -192,7 +192,8 @@ struct ucred {
 #define AF_IEEE802154  36      /* IEEE802154 sockets           */
 #define AF_CAIF                37      /* CAIF sockets                 */
 #define AF_ALG         38      /* Algorithm sockets            */
-#define AF_MAX         39      /* For now.. */
+#define AF_MSM_IPC     39      /* MSM_IPC sockets              */
+#define AF_MAX         40      /* For now.. */
 
 /* Protocol families, same as address families. */
 #define PF_UNSPEC      AF_UNSPEC
@@ -234,6 +235,7 @@ struct ucred {
 #define PF_IEEE802154  AF_IEEE802154
 #define PF_CAIF                AF_CAIF
 #define PF_ALG         AF_ALG
+#define PF_MSM_IPC     AF_MSM_IPC
 #define PF_MAX         AF_MAX
 
 /* Maximum queue length specifiable by listen.  */
@@ -308,6 +310,7 @@ struct ucred {
 #define SOL_IUCV       277
 #define SOL_CAIF       278
 #define SOL_ALG                279
+#define SOL_MSM_IPC    280
 
 /* IPX options */
 #define IPX_TYPE       1
diff --git a/net/msm_ipc/Makefile b/net/msm_ipc/Makefile
index 461c510..b9a0dfb 100644
--- a/net/msm_ipc/Makefile
+++ b/net/msm_ipc/Makefile
@@ -1,3 +1,3 @@
 obj-$(CONFIG_MSM_IPC) := msm_ipc.o
 
-msm_ipc-y += msm_ipc_router.o
+msm_ipc-y += msm_ipc_router.o msm_ipc_socket.o
diff --git a/net/msm_ipc/msm_ipc_router.c b/net/msm_ipc/msm_ipc_router.c
index 76deacb..0cd0a02 100644
--- a/net/msm_ipc/msm_ipc_router.c
+++ b/net/msm_ipc/msm_ipc_router.c
@@ -1638,7 +1638,7 @@ int msm_ipc_router_close(void)
 
 static int __init msm_ipc_router_init(void)
 {
-       int i;
+       int i, ret;
        struct msm_ipc_routing_table_entry *rt_entry;
 #if !defined(CONFIG_MSM_IPC_ROUTER_SMD_XPRT)
        struct work_struct dummy_work;
@@ -1662,8 +1662,11 @@ static int __init msm_ipc_router_init(void)
        mutex_unlock(&routing_table_lock);
 
        init_waitqueue_head(&newserver_wait);
+       ret = msm_ipc_router_init_sockets();
+       if (ret < 0)
+               pr_err("%s: Init sockets failed\n", __func__);
 
-       return 0;
+       return ret;
 }
 
 module_init(msm_ipc_router_init);
diff --git a/net/msm_ipc/msm_ipc_router.h b/net/msm_ipc/msm_ipc_router.h
index 4d30c8f..ef47f11 100644
--- a/net/msm_ipc/msm_ipc_router.h
+++ b/net/msm_ipc/msm_ipc_router.h
@@ -132,6 +132,11 @@ struct msm_ipc_port {
        void *priv;
 };
 
+struct msm_ipc_sock {
+       struct sock sk;
+       struct msm_ipc_port *port;
+};
+
 struct msm_ipc_router_xprt {
        char *name;
        uint32_t link_id;
@@ -184,4 +189,7 @@ int msm_ipc_router_register_server(struct msm_ipc_port 
*server_port,
                            struct msm_ipc_addr *name);
 int msm_ipc_router_unregister_server(struct msm_ipc_port *server_port);
 
+int msm_ipc_router_init_sockets(void);
+void msm_ipc_router_exit_sockets(void);
+
 #endif
diff --git a/net/msm_ipc/msm_ipc_socket.c b/net/msm_ipc/msm_ipc_socket.c
new file mode 100644
index 0000000..9d98691
--- /dev/null
+++ b/net/msm_ipc/msm_ipc_socket.c
@@ -0,0 +1,520 @@
+/* Copyright (c) 2011, Code Aurora Forum. All rights reserved.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 and
+ * only version 2 as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ */
+
+#include <linux/module.h>
+#include <linux/types.h>
+#include <linux/net.h>
+#include <linux/socket.h>
+#include <linux/errno.h>
+#include <linux/mm.h>
+#include <linux/poll.h>
+#include <linux/fcntl.h>
+#include <linux/gfp.h>
+#include <linux/msm_ipc.h>
+#include <linux/string.h>
+#include <linux/atomic.h>
+
+#include <net/sock.h>
+
+#include "msm_ipc_router.h"
+
+#define msm_ipc_sk(sk) ((struct msm_ipc_sock *)(sk))
+#define msm_ipc_sk_port(sk) ((struct msm_ipc_port *)(msm_ipc_sk(sk)->port))
+#define MODEM_LOAD_TIMEOUT (10 * HZ)
+
+static int sockets_enabled;
+static struct proto msm_ipc_proto;
+static const struct proto_ops msm_ipc_proto_ops;
+
+static struct sk_buff_head *msm_ipc_router_build_msg(unsigned int num_sect,
+                                         struct iovec const *msg_sect,
+                                         size_t total_len)
+{
+       struct sk_buff_head *msg_head;
+       struct sk_buff *msg;
+       int i, copied, first = 1;
+       int data_size = 0, request_size, offset;
+       void *data;
+
+       for (i = 0; i < num_sect; i++)
+               data_size += msg_sect[i].iov_len;
+
+       if (!data_size)
+               return NULL;
+
+       msg_head = kmalloc(sizeof(struct sk_buff_head), GFP_KERNEL);
+       if (!msg_head) {
+               pr_err("%s: cannot allocate skb_head\n", __func__);
+               return NULL;
+       }
+       skb_queue_head_init(msg_head);
+
+       for (copied = 1, i = 0; copied && (i < num_sect); i++) {
+               data_size = msg_sect[i].iov_len;
+               offset = 0;
+               while (offset != msg_sect[i].iov_len) {
+                       request_size = data_size;
+                       if (first)
+                               request_size += IPC_ROUTER_HDR_SIZE;
+
+                       msg = alloc_skb(request_size, GFP_KERNEL);
+                       if (!msg) {
+                               if (request_size <= (PAGE_SIZE/2)) {
+                                       pr_err("%s: cannot allocated skb\n",
+                                               __func__);
+                                       goto msg_build_failure;
+                               }
+                               data_size = data_size / 2;
+                               continue;
+                       }
+
+                       if (first) {
+                               skb_reserve(msg, IPC_ROUTER_HDR_SIZE);
+                               first = 0;
+                       }
+
+                       data = skb_put(msg, data_size);
+                       copied = !copy_from_user(msg->data,
+                                       msg_sect[i].iov_base + offset,
+                                       data_size);
+                       if (!copied) {
+                               pr_err("%s: copy_from_user failed\n",
+                                       __func__);
+                               kfree_skb(msg);
+                               goto msg_build_failure;
+                       }
+                       skb_queue_tail(msg_head, msg);
+                       offset += data_size;
+                       data_size = msg_sect[i].iov_len - offset;
+               }
+       }
+       return msg_head;
+
+msg_build_failure:
+       while (!skb_queue_empty(msg_head)) {
+               msg = skb_dequeue(msg_head);
+               kfree_skb(msg);
+       }
+       kfree(msg_head);
+       return NULL;
+}
+
+static int msm_ipc_router_extract_msg(struct msghdr *m,
+                                     struct sk_buff_head *msg_head)
+{
+       struct sockaddr_msm_ipc *addr = (struct sockaddr_msm_ipc *)m->msg_name;
+       struct rr_header *hdr;
+       struct sk_buff *temp;
+       int offset = 0, data_len = 0, copy_len;
+
+       if (!m || !msg_head) {
+               pr_err("%s: Invalid pointers passed\n", __func__);
+               return -EINVAL;
+       }
+
+       temp = skb_peek(msg_head);
+       hdr = (struct rr_header *)(temp->data);
+       if (addr || (hdr->src_port_id != IPC_ROUTER_ADDRESS)) {
+               addr->family = AF_MSM_IPC;
+               addr->address.addrtype = MSM_IPC_ADDR_ID;
+               addr->address.addr.port_addr.node_id = hdr->src_node_id;
+               addr->address.addr.port_addr.port_id = hdr->src_port_id;
+               m->msg_namelen = sizeof(struct sockaddr_msm_ipc);
+       }
+
+       data_len = hdr->size;
+       skb_pull(temp, IPC_ROUTER_HDR_SIZE);
+       skb_queue_walk(msg_head, temp) {
+               copy_len = data_len < temp->len ? data_len : temp->len;
+               if (copy_to_user(m->msg_iov->iov_base + offset, temp->data,
+                                copy_len)) {
+                       pr_err("%s: Copy to user failed\n", __func__);
+                       return -EFAULT;
+               }
+               offset += copy_len;
+               data_len -= copy_len;
+       }
+       return offset;
+}
+
+static void msm_ipc_router_release_msg(struct sk_buff_head *msg_head)
+{
+       struct sk_buff *temp;
+
+       if (!msg_head) {
+               pr_err("%s: Invalid msg pointer\n", __func__);
+               return;
+       }
+
+       while (!skb_queue_empty(msg_head)) {
+               temp = skb_dequeue(msg_head);
+               kfree_skb(temp);
+       }
+       kfree(msg_head);
+}
+
+static int msm_ipc_router_create(struct net *net,
+                                struct socket *sock,
+                                int protocol,
+                                int kern)
+{
+       struct sock *sk;
+       struct msm_ipc_port *port_ptr;
+
+       if (unlikely(protocol != 0)) {
+               pr_err("%s: Protocol not supported\n", __func__);
+               return -EPROTONOSUPPORT;
+       }
+
+       switch (sock->type) {
+       case SOCK_DGRAM:
+               break;
+       default:
+               pr_err("%s: Protocol type not supported\n", __func__);
+               return -EPROTOTYPE;
+       }
+
+       sk = sk_alloc(net, AF_MSM_IPC, GFP_KERNEL, &msm_ipc_proto);
+       if (!sk) {
+               pr_err("%s: sk_alloc failed\n", __func__);
+               return -ENOMEM;
+       }
+
+       port_ptr = msm_ipc_router_create_raw_port(sk, NULL, NULL);
+       if (!port_ptr) {
+               pr_err("%s: port_ptr alloc failed\n", __func__);
+               sk_free(sk);
+               return -ENOMEM;
+       }
+
+       sock->ops = &msm_ipc_proto_ops;
+       sock_init_data(sock, sk);
+       sk->sk_rcvtimeo = DEFAULT_RCV_TIMEO;
+
+       msm_ipc_sk(sk)->port = port_ptr;
+
+       return 0;
+}
+
+int msm_ipc_router_bind(struct socket *sock, struct sockaddr *uaddr,
+                              int uaddr_len)
+{
+       struct sockaddr_msm_ipc *addr = (struct sockaddr_msm_ipc *)uaddr;
+       struct sock *sk = sock->sk;
+       struct msm_ipc_port *port_ptr;
+       int ret;
+
+       if (!sk)
+               return -EINVAL;
+
+       if (!uaddr_len) {
+               pr_err("%s: Invalid address length\n", __func__);
+               return -EINVAL;
+       }
+
+       if (addr->family != AF_MSM_IPC) {
+               pr_err("%s: Address family is incorrect\n", __func__);
+               return -EAFNOSUPPORT;
+       }
+
+       if (addr->address.addrtype != MSM_IPC_ADDR_NAME) {
+               pr_err("%s: Address type is incorrect\n", __func__);
+               return -EINVAL;
+       }
+
+       port_ptr = msm_ipc_sk_port(sk);
+       if (!port_ptr)
+               return -ENODEV;
+
+       lock_sock(sk);
+
+       ret = msm_ipc_router_register_server(port_ptr, &addr->address);
+       if (!ret)
+               sk->sk_rcvtimeo = DEFAULT_RCV_TIMEO;
+
+       release_sock(sk);
+       return ret;
+}
+
+static int msm_ipc_router_sendmsg(struct kiocb *iocb, struct socket *sock,
+                                 struct msghdr *m, size_t total_len)
+{
+       struct sock *sk = sock->sk;
+       struct msm_ipc_port *port_ptr = msm_ipc_sk_port(sk);
+       struct sockaddr_msm_ipc *dest = (struct sockaddr_msm_ipc *)m->msg_name;
+       struct sk_buff_head *msg;
+       int ret;
+
+       if (!dest)
+               return -EDESTADDRREQ;
+
+       if (m->msg_namelen < sizeof(*dest) || dest->family != AF_MSM_IPC)
+               return -EINVAL;
+
+       if (total_len > MAX_IPC_PKT_SIZE)
+               return -EINVAL;
+
+       lock_sock(sk);
+       msg = msm_ipc_router_build_msg(m->msg_iovlen, m->msg_iov, total_len);
+       if (!msg) {
+               pr_err("%s: Msg build failure\n", __func__);
+               ret = -ENOMEM;
+               goto out_sendmsg;
+       }
+
+       ret = msm_ipc_router_send_to(port_ptr, msg, &dest->address);
+       if (ret == (IPC_ROUTER_HDR_SIZE + total_len))
+               ret = total_len;
+
+out_sendmsg:
+       release_sock(sk);
+       return ret;
+}
+
+static int msm_ipc_router_recvmsg(struct kiocb *iocb, struct socket *sock,
+                                 struct msghdr *m, size_t buf_len, int flags)
+{
+       struct sock *sk = sock->sk;
+       struct msm_ipc_port *port_ptr = msm_ipc_sk_port(sk);
+       struct sk_buff_head *msg;
+       long timeout;
+       int ret;
+
+       if (m->msg_iovlen != 1)
+               return -EOPNOTSUPP;
+
+       if (!buf_len)
+               return -EINVAL;
+
+       lock_sock(sk);
+       timeout = sk->sk_rcvtimeo;
+       mutex_lock(&port_ptr->port_rx_q_lock);
+       while (list_empty(&port_ptr->port_rx_q)) {
+               mutex_unlock(&port_ptr->port_rx_q_lock);
+               release_sock(sk);
+               if (timeout < 0) {
+                       ret = wait_event_interruptible(
+                                       port_ptr->port_rx_wait_q,
+                                       !list_empty(&port_ptr->port_rx_q));
+                       if (ret)
+                               return ret;
+               } else if (timeout > 0) {
+                       timeout = wait_event_interruptible_timeout(
+                                       port_ptr->port_rx_wait_q,
+                                       !list_empty(&port_ptr->port_rx_q),
+                                       timeout);
+                       if (timeout < 0)
+                               return -EFAULT;
+               }
+
+               if (timeout == 0)
+                       return -ETIMEDOUT;
+               lock_sock(sk);
+               mutex_lock(&port_ptr->port_rx_q_lock);
+       }
+       mutex_unlock(&port_ptr->port_rx_q_lock);
+
+       ret = msm_ipc_router_read(port_ptr, &msg, buf_len);
+       if (ret <= 0 || !msg) {
+               release_sock(sk);
+               return ret;
+       }
+
+       ret = msm_ipc_router_extract_msg(m, msg);
+       msm_ipc_router_release_msg(msg);
+       msg = NULL;
+       release_sock(sk);
+       return ret;
+}
+
+static int msm_ipc_router_ioctl(struct socket *sock,
+                               unsigned int cmd, unsigned long arg)
+{
+       struct sock *sk = sock->sk;
+       struct msm_ipc_port *port_ptr;
+       struct server_lookup_args server_arg;
+       struct msm_ipc_port_addr *port_addr = NULL;
+       unsigned int n, port_addr_sz = 0;
+       int ret;
+
+       if (!sk)
+               return -EINVAL;
+
+       lock_sock(sk);
+       port_ptr = msm_ipc_sk_port(sock->sk);
+       if (!port_ptr) {
+               release_sock(sk);
+               return -EINVAL;
+       }
+
+       switch (cmd) {
+       case IPC_ROUTER_IOCTL_GET_VERSION:
+               n = IPC_ROUTER_VERSION;
+               ret = put_user(n, (unsigned int *)arg);
+               break;
+
+       case IPC_ROUTER_IOCTL_GET_MTU:
+               n = (MAX_IPC_PKT_SIZE - IPC_ROUTER_HDR_SIZE);
+               ret = put_user(n, (unsigned int *)arg);
+               break;
+
+       case IPC_ROUTER_IOCTL_GET_CURR_PKT_SIZE:
+               ret = msm_ipc_router_get_curr_pkt_size(port_ptr);
+               break;
+
+       case IPC_ROUTER_IOCTL_LOOKUP_SERVER:
+               ret = copy_from_user(&server_arg, (void *)arg,
+                                    sizeof(server_arg));
+               if (ret) {
+                       ret = -EFAULT;
+                       break;
+               }
+
+               if (server_arg.num_entries_in_array < 0) {
+                       ret = -EINVAL;
+                       break;
+               }
+               if (server_arg.num_entries_in_array) {
+                       port_addr_sz = server_arg.num_entries_in_array *
+                                       sizeof(*port_addr);
+                       port_addr = kmalloc(port_addr_sz, GFP_KERNEL);
+                       if (!port_addr) {
+                               ret = -ENOMEM;
+                               break;
+                       }
+               }
+               ret = msm_ipc_router_lookup_server_name(&server_arg.port_name,
+                               port_addr, server_arg.num_entries_in_array);
+               if (ret < 0) {
+                       pr_err("%s: Server not found\n", __func__);
+                       ret = -ENODEV;
+                       kfree(port_addr);
+                       break;
+               }
+               server_arg.num_entries_found = ret;
+
+               ret = copy_to_user((void *)arg, &server_arg,
+                                  sizeof(server_arg));
+               if (port_addr_sz) {
+                       ret = copy_to_user((void *)(arg + sizeof(server_arg)),
+                                          port_addr, port_addr_sz);
+                       if (ret)
+                               ret = -EFAULT;
+                       kfree(port_addr);
+               }
+               break;
+
+       case IPC_ROUTER_IOCTL_BIND_CONTROL_PORT:
+               ret = msm_ipc_router_bind_control_port(port_ptr);
+               break;
+
+       default:
+               ret = -EINVAL;
+       }
+       release_sock(sk);
+       return ret;
+}
+
+static unsigned int msm_ipc_router_poll(struct file *file,
+                       struct socket *sock, poll_table *wait)
+{
+       struct sock *sk = sock->sk;
+       struct msm_ipc_port *port_ptr;
+       uint32_t mask = 0;
+
+       if (!sk)
+               return -EINVAL;
+
+       port_ptr = msm_ipc_sk_port(sk);
+       if (!port_ptr)
+               return -EINVAL;
+
+       poll_wait(file, &port_ptr->port_rx_wait_q, wait);
+
+       if (!list_empty(&port_ptr->port_rx_q))
+               mask |= (POLLRDNORM | POLLIN);
+
+       return mask;
+}
+
+static int msm_ipc_router_close(struct socket *sock)
+{
+       struct sock *sk = sock->sk;
+       struct msm_ipc_port *port_ptr = msm_ipc_sk_port(sk);
+       int ret;
+
+       lock_sock(sk);
+       ret = msm_ipc_router_close_port(port_ptr);
+       release_sock(sk);
+       sock_put(sk);
+       sock->sk = NULL;
+
+       return ret;
+}
+
+static const struct net_proto_family msm_ipc_family_ops = {
+       .owner          = THIS_MODULE,
+       .family         = AF_MSM_IPC,
+       .create         = msm_ipc_router_create
+};
+
+static const struct proto_ops msm_ipc_proto_ops = {
+       .owner          = THIS_MODULE,
+       .family         = AF_MSM_IPC,
+       .bind           = msm_ipc_router_bind,
+       .connect        = sock_no_connect,
+       .sendmsg        = msm_ipc_router_sendmsg,
+       .recvmsg        = msm_ipc_router_recvmsg,
+       .ioctl          = msm_ipc_router_ioctl,
+       .poll           = msm_ipc_router_poll,
+       .setsockopt     = sock_no_setsockopt,
+       .getsockopt     = sock_no_getsockopt,
+       .release        = msm_ipc_router_close,
+};
+
+static struct proto msm_ipc_proto = {
+       .name           = "MSM_IPC",
+       .owner          = THIS_MODULE,
+       .obj_size       = sizeof(struct msm_ipc_sock),
+};
+
+int msm_ipc_router_init_sockets(void)
+{
+       int ret;
+
+       ret = proto_register(&msm_ipc_proto, 1);
+       if (ret) {
+               pr_err("Failed to register MSM_IPC protocol type\n");
+               goto out_init_sockets;
+       }
+
+       ret = sock_register(&msm_ipc_family_ops);
+       if (ret) {
+               pr_err("Failed to register MSM_IPC socket type\n");
+               proto_unregister(&msm_ipc_proto);
+               goto out_init_sockets;
+       }
+
+       sockets_enabled = 1;
+out_init_sockets:
+       return ret;
+}
+
+void msm_ipc_router_exit_sockets(void)
+{
+       if (!sockets_enabled)
+               return;
+
+       sockets_enabled = 0;
+       sock_unregister(msm_ipc_family_ops.family);
+       proto_unregister(&msm_ipc_proto);
+}
-- 
1.7.3.3

Sent by an employee of the Qualcomm Innovation Center, Inc.
The Qualcomm Innovation Center, Inc. is a member of the Code Aurora Forum.
--
To unsubscribe from this list: send the line "unsubscribe linux-arm-msm" in
the body of a message to [email protected]
More majordomo info at  http://vger.kernel.org/majordomo-info.html

Reply via email to