|
diff -Naur a/modules/proxy/config.m4 b/modules/proxy/config.m4 |
|
--- a/modules/proxy/config.m4 |
|
+++ b/modules/proxy/config.m4 |
|
@@ -18,6 +18,7 @@ |
|
proxy_http_objs="mod_proxy_http.lo" |
|
proxy_scgi_objs="mod_proxy_scgi.lo" |
|
proxy_ajp_objs="mod_proxy_ajp.lo ajp_header.lo ajp_link.lo ajp_msg.lo ajp_utils.lo" |
|
+proxy_wstunnel_objs="mod_proxy_wstunnel.lo" |
|
proxy_balancer_objs="mod_proxy_balancer.lo" |
|
|
|
case "$host" in |
|
@@ -29,6 +30,7 @@ |
|
proxy_http_objs="$proxy_http_objs mod_proxy.la" |
|
proxy_scgi_objs="$proxy_scgi_objs mod_proxy.la" |
|
proxy_ajp_objs="$proxy_ajp_objs mod_proxy.la" |
|
+ proxy_wstunnel_objs="$proxy_wstunnel_objs mod_proxy.la" |
|
proxy_balancer_objs="$proxy_balancer_objs mod_proxy.la" |
|
;; |
|
esac |
|
@@ -37,6 +39,7 @@ |
|
APACHE_MODULE(proxy_ftp, Apache proxy FTP module, $proxy_ftp_objs, , $proxy_mods_enable) |
|
APACHE_MODULE(proxy_http, Apache proxy HTTP module, $proxy_http_objs, , $proxy_mods_enable) |
|
APACHE_MODULE(proxy_scgi, Apache proxy SCGI module, $proxy_scgi_objs, , $proxy_mods_enable) |
|
+APACHE_MODULE(proxy_wstunnel, Apache proxy Websocket Tunnel module, $proxy_wstunnel_objs, , $proxy_mods_enable) |
|
APACHE_MODULE(proxy_ajp, Apache proxy AJP module, $proxy_ajp_objs, , $proxy_mods_enable) |
|
APACHE_MODULE(proxy_balancer, Apache proxy BALANCER module, $proxy_balancer_objs, , $proxy_mods_enable) |
|
|
|
diff -Naur a/modules/proxy/mod_proxy_wstunnel.h b/modules/proxy/mod_proxy_wstunnel.h |
|
--- a/modules/proxy/mod_proxy_wstunnel.h |
|
+++ b/modules/proxy/mod_proxy_wstunnel.h |
|
@@ -0,0 +1,88 @@ |
|
+/* Licensed to the Apache Software Foundation (ASF) under one or more |
|
+ * contributor license agreements. See the NOTICE file distributed with |
|
+ * this work for additional information regarding copyright ownership. |
|
+ * The ASF licenses this file to You under the Apache License, Version 2.0 |
|
+ * (the "License"); you may not use this file except in compliance with |
|
+ * the License. You may obtain a copy of the License at |
|
+ * |
|
+ * http://www.apache.org/licenses/LICENSE-2.0 |
|
+ * |
|
+ * Unless required by applicable law or agreed to in writing, software |
|
+ * distributed under the License is distributed on an "AS IS" BASIS, |
|
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
+ * See the License for the specific language governing permissions and |
|
+ * limitations under the License. |
|
+ */ |
|
+ |
|
+#ifndef MOD_PROXY_WSTUNNEL_H |
|
+#define MOD_PROXY_WSTUNNEL_H |
|
+ |
|
+/** |
|
+ * @file mod_proxy_wstunnel.h |
|
+ * @brief Proxy Extension Module for Apache |
|
+ * |
|
+ * @defgroup MOD_PROXY_WSTUNNEL mod_proxy_wstunnel |
|
+ * @ingroup APACHE_MODS |
|
+ * @{ |
|
+ */ |
|
+ |
|
+/* |
|
+ |
|
+ Also note numerous FIXMEs and CHECKMEs which should be eliminated. |
|
+ |
|
+ This code is once again experimental! |
|
+ |
|
+ Things to do: |
|
+ |
|
+ 1. Make it completely work (for FTP too) |
|
+ |
|
+ 2. HTTP/1.1 |
|
+ |
|
+ Chuck Murcko <chuck@topsail.org> 02-06-01 |
|
+ |
|
+ */ |
|
+ |
|
+#include "mod_proxy.h" |
|
+ |
|
+/** |
|
+ * Create a HTTP request header brigade, old_cl_val and old_te_val as required. |
|
+ * @parama p pool |
|
+ * @param header_brigade header brigade to use/fill |
|
+ * @param r request |
|
+ * @param p_conn proxy connection rec |
|
+ * @param worker selected worker |
|
+ * @param conf per-server proxy config |
|
+ * @param uri uri |
|
+ * @param url url |
|
+ * @param server_portstr port as string |
|
+ * @param old_cl_val stored old content-len val |
|
+ * @param old_te_val stored old TE val |
|
+ * @return OK or HTTP_EXPECTATION_FAILED |
|
+ */ |
|
+PROXY_DECLARE(int) ap_proxy_create_hdrbrgd(apr_pool_t *p, |
|
+ apr_bucket_brigade *header_brigade, |
|
+ request_rec *r, |
|
+ proxy_conn_rec *p_conn, |
|
+ proxy_worker *worker, |
|
+ proxy_server_conf *conf, |
|
+ apr_uri_t *uri, |
|
+ char *url, char *server_portstr, |
|
+ char **old_cl_val, |
|
+ char **old_te_val); |
|
+ |
|
+/** |
|
+ * @param bucket_alloc bucket allocator |
|
+ * @param r request |
|
+ * @param p_conn proxy connection |
|
+ * @param origin connection rec of origin |
|
+ * @param bb brigade to send to origin |
|
+ * @param flush flush |
|
+ * @return status (OK) |
|
+ */ |
|
+PROXY_DECLARE(int) ap_proxy_pass_brigade(apr_bucket_alloc_t *bucket_alloc, |
|
+ request_rec *r, proxy_conn_rec *p_conn, |
|
+ conn_rec *origin, apr_bucket_brigade *bb, |
|
+ int flush); |
|
+ |
|
+#endif /*MOD_PROXY_WSTUNNEL_H*/ |
|
+/** @} */ |
|
diff -Naur a/modules/proxy/mod_proxy_wstunnel.c b/modules/proxy/mod_proxy_wstunnel.c |
|
--- a/modules/proxy/mod_proxy_wstunnel.c |
|
+++ b/modules/proxy/mod_proxy_wstunnel.c |
|
@@ -0,0 +1,728 @@ |
|
+/* Licensed to the Apache Software Foundation (ASF) under one or more |
|
+ * contributor license agreements. See the NOTICE file distributed with |
|
+ * this work for additional information regarding copyright ownership. |
|
+ * The ASF licenses this file to You under the Apache License, Version 2.0 |
|
+ * (the "License"); you may not use this file except in compliance with |
|
+ * the License. You may obtain a copy of the License at |
|
+ * |
|
+ * http://www.apache.org/licenses/LICENSE-2.0 |
|
+ * |
|
+ * Unless required by applicable law or agreed to in writing, software |
|
+ * distributed under the License is distributed on an "AS IS" BASIS, |
|
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
+ * See the License for the specific language governing permissions and |
|
+ * limitations under the License. |
|
+ */ |
|
+ |
|
+#include "mod_proxy_wstunnel.h" |
|
+ |
|
+module AP_MODULE_DECLARE_DATA proxy_wstunnel_module; |
|
+ |
|
+/* Clear all connection-based headers from the incoming headers table */ |
|
+typedef struct header_dptr { |
|
+ apr_pool_t *pool; |
|
+ apr_table_t *table; |
|
+ apr_time_t time; |
|
+} header_dptr; |
|
+ |
|
+static int clear_conn_headers(void *data, const char *key, const char *val) |
|
+{ |
|
+ apr_table_t *headers = ((header_dptr*)data)->table; |
|
+ apr_pool_t *pool = ((header_dptr*)data)->pool; |
|
+ const char *name; |
|
+ char *next = apr_pstrdup(pool, val); |
|
+ while (*next) { |
|
+ name = next; |
|
+ while (*next && !apr_isspace(*next) && (*next != ',')) { |
|
+ ++next; |
|
+ } |
|
+ while (*next && (apr_isspace(*next) || (*next == ','))) { |
|
+ *next++ = '\0'; |
|
+ } |
|
+ apr_table_unset(headers, name); |
|
+ } |
|
+ return 1; |
|
+} |
|
+ |
|
+static void proxy_clear_connection(apr_pool_t *p, apr_table_t *headers) |
|
+{ |
|
+ header_dptr x; |
|
+ x.pool = p; |
|
+ x.table = headers; |
|
+ apr_table_unset(headers, "Proxy-Connection"); |
|
+ apr_table_do(clear_conn_headers, &x, headers, "Connection", NULL); |
|
+ apr_table_unset(headers, "Connection"); |
|
+} |
|
+ |
|
+PROXY_DECLARE(int) ap_proxy_create_hdrbrgd(apr_pool_t *p, |
|
+ apr_bucket_brigade *header_brigade, |
|
+ request_rec *r, |
|
+ proxy_conn_rec *p_conn, |
|
+ proxy_worker *worker, |
|
+ proxy_server_conf *conf, |
|
+ apr_uri_t *uri, |
|
+ char *url, char *server_portstr, |
|
+ char **old_cl_val, |
|
+ char **old_te_val) |
|
+{ |
|
+ conn_rec *c = r->connection; |
|
+ int counter; |
|
+ char *buf; |
|
+ const apr_array_header_t *headers_in_array; |
|
+ const apr_table_entry_t *headers_in; |
|
+ apr_table_t *headers_in_copy; |
|
+ apr_bucket *e; |
|
+ int do_100_continue; |
|
+ conn_rec *origin = p_conn->connection; |
|
+ proxy_dir_conf *dconf = ap_get_module_config(r->per_dir_config, &proxy_module); |
|
+ |
|
+ /* |
|
+ * To be compliant, we only use 100-Continue for requests with bodies. |
|
+ * We also make sure we won't be talking HTTP/1.0 as well. |
|
+ */ |
|
+ do_100_continue = (worker->ping_timeout_set |
|
+ && !r->header_only |
|
+ && (apr_table_get(r->headers_in, "Content-Length") |
|
+ || apr_table_get(r->headers_in, "Transfer-Encoding")) |
|
+ && (PROXYREQ_REVERSE == r->proxyreq) |
|
+ && !(apr_table_get(r->subprocess_env, "force-proxy-request-1.0"))); |
|
+ |
|
+ if (apr_table_get(r->subprocess_env, "force-proxy-request-1.0")) { |
|
+ /* |
|
+ * According to RFC 2616 8.2.3 we are not allowed to forward an |
|
+ * Expect: 100-continue to an HTTP/1.0 server. Instead we MUST return |
|
+ * a HTTP_EXPECTATION_FAILED |
|
+ */ |
|
+ if (r->expecting_100) { |
|
+ return HTTP_EXPECTATION_FAILED; |
|
+ } |
|
+ buf = apr_pstrcat(p, r->method, " ", url, " HTTP/1.0" CRLF, NULL); |
|
+ p_conn->close = 1; |
|
+ } else { |
|
+ buf = apr_pstrcat(p, r->method, " ", url, " HTTP/1.1" CRLF, NULL); |
|
+ } |
|
+ if (apr_table_get(r->subprocess_env, "proxy-nokeepalive")) { |
|
+ origin->keepalive = AP_CONN_CLOSE; |
|
+ p_conn->close = 1; |
|
+ } |
|
+ ap_xlate_proto_to_ascii(buf, strlen(buf)); |
|
+ e = apr_bucket_pool_create(buf, strlen(buf), p, c->bucket_alloc); |
|
+ APR_BRIGADE_INSERT_TAIL(header_brigade, e); |
|
+ if (conf->preserve_host == 0) { |
|
+ if (ap_strchr_c(uri->hostname, ':')) { /* if literal IPv6 address */ |
|
+ if (uri->port_str && uri->port != DEFAULT_HTTP_PORT) { |
|
+ buf = apr_pstrcat(p, "Host: [", uri->hostname, "]:", |
|
+ uri->port_str, CRLF, NULL); |
|
+ } else { |
|
+ buf = apr_pstrcat(p, "Host: [", uri->hostname, "]", CRLF, NULL); |
|
+ } |
|
+ } else { |
|
+ if (uri->port_str && uri->port != DEFAULT_HTTP_PORT) { |
|
+ buf = apr_pstrcat(p, "Host: ", uri->hostname, ":", |
|
+ uri->port_str, CRLF, NULL); |
|
+ } else { |
|
+ buf = apr_pstrcat(p, "Host: ", uri->hostname, CRLF, NULL); |
|
+ } |
|
+ } |
|
+ } |
|
+ else { |
|
+ /* don't want to use r->hostname, as the incoming header might have a |
|
+ * port attached |
|
+ */ |
|
+ const char* hostname = apr_table_get(r->headers_in,"Host"); |
|
+ if (!hostname) { |
|
+ hostname = r->server->server_hostname; |
|
+ ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, "AH01092: " |
|
+ "no HTTP 0.9 request (with no host line) " |
|
+ "on incoming request and preserve host set " |
|
+ "forcing hostname to be %s for uri %s", |
|
+ hostname, r->uri); |
|
+ } |
|
+ buf = apr_pstrcat(p, "Host: ", hostname, CRLF, NULL); |
|
+ } |
|
+ ap_xlate_proto_to_ascii(buf, strlen(buf)); |
|
+ e = apr_bucket_pool_create(buf, strlen(buf), p, c->bucket_alloc); |
|
+ APR_BRIGADE_INSERT_TAIL(header_brigade, e); |
|
+ |
|
+ /* handle Via */ |
|
+ if (conf->viaopt == via_block) { |
|
+ /* Block all outgoing Via: headers */ |
|
+ apr_table_unset(r->headers_in, "Via"); |
|
+ } else if (conf->viaopt != via_off) { |
|
+ const char *server_name = ap_get_server_name(r); |
|
+ /* If USE_CANONICAL_NAME_OFF was configured for the proxy virtual host, |
|
+ * then the server name returned by ap_get_server_name() is the |
|
+ * origin server name (which does make too much sense with Via: headers) |
|
+ * so we use the proxy vhost's name instead. |
|
+ */ |
|
+ if (server_name == r->hostname) |
|
+ server_name = r->server->server_hostname; |
|
+ /* Create a "Via:" request header entry and merge it */ |
|
+ /* Generate outgoing Via: header with/without server comment: */ |
|
+ apr_table_mergen(r->headers_in, "Via", |
|
+ (conf->viaopt == via_full) |
|
+ ? apr_psprintf(p, "%d.%d %s%s (%s)", |
|
+ HTTP_VERSION_MAJOR(r->proto_num), |
|
+ HTTP_VERSION_MINOR(r->proto_num), |
|
+ server_name, server_portstr, |
|
+ AP_SERVER_BASEVERSION) |
|
+ : apr_psprintf(p, "%d.%d %s%s", |
|
+ HTTP_VERSION_MAJOR(r->proto_num), |
|
+ HTTP_VERSION_MINOR(r->proto_num), |
|
+ server_name, server_portstr) |
|
+ ); |
|
+ } |
|
+ |
|
+ /* Use HTTP/1.1 100-Continue as quick "HTTP ping" test |
|
+ * to backend |
|
+ */ |
|
+ if (do_100_continue) { |
|
+ apr_table_mergen(r->headers_in, "Expect", "100-Continue"); |
|
+ r->expecting_100 = 1; |
|
+ } |
|
+ |
|
+ /* X-Forwarded-*: handling |
|
+ * |
|
+ * XXX Privacy Note: |
|
+ * ----------------- |
|
+ * |
|
+ * These request headers are only really useful when the mod_proxy |
|
+ * is used in a reverse proxy configuration, so that useful info |
|
+ * about the client can be passed through the reverse proxy and on |
|
+ * to the backend server, which may require the information to |
|
+ * function properly. |
|
+ * |
|
+ * In a forward proxy situation, these options are a potential |
|
+ * privacy violation, as information about clients behind the proxy |
|
+ * are revealed to arbitrary servers out there on the internet. |
|
+ * |
|
+ * The HTTP/1.1 Via: header is designed for passing client |
|
+ * information through proxies to a server, and should be used in |
|
+ * a forward proxy configuation instead of X-Forwarded-*. See the |
|
+ * ProxyVia option for details. |
|
+ */ |
|
+ if (PROXYREQ_REVERSE == r->proxyreq) { |
|
+ const char *buf; |
|
+ |
|
+ /* Add X-Forwarded-For: so that the upstream has a chance to |
|
+ * determine, where the original request came from. |
|
+ */ |
|
+ apr_table_mergen(r->headers_in, "X-Forwarded-For", |
|
+ c->remote_ip); |
|
+ |
|
+ /* Add X-Forwarded-Host: so that upstream knows what the |
|
+ * original request hostname was. |
|
+ */ |
|
+ if ((buf = apr_table_get(r->headers_in, "Host"))) { |
|
+ apr_table_mergen(r->headers_in, "X-Forwarded-Host", buf); |
|
+ } |
|
+ |
|
+ /* Add X-Forwarded-Server: so that upstream knows what the |
|
+ * name of this proxy server is (if there are more than one) |
|
+ * XXX: This duplicates Via: - do we strictly need it? |
|
+ */ |
|
+ apr_table_mergen(r->headers_in, "X-Forwarded-Server", |
|
+ r->server->server_hostname); |
|
+ } |
|
+ |
|
+ proxy_run_fixups(r); |
|
+ /* |
|
+ * Make a copy of the headers_in table before clearing the connection |
|
+ * headers as we need the connection headers later in the http output |
|
+ * filter to prepare the correct response headers. |
|
+ * |
|
+ * Note: We need to take r->pool for apr_table_copy as the key / value |
|
+ * pairs in r->headers_in have been created out of r->pool and |
|
+ * p might be (and actually is) a longer living pool. |
|
+ * This would trigger the bad pool ancestry abort in apr_table_copy if |
|
+ * apr is compiled with APR_POOL_DEBUG. |
|
+ */ |
|
+ headers_in_copy = apr_table_copy(r->pool, r->headers_in); |
|
+ proxy_clear_connection(p, headers_in_copy); |
|
+ /* send request headers */ |
|
+ headers_in_array = apr_table_elts(headers_in_copy); |
|
+ headers_in = (const apr_table_entry_t *) headers_in_array->elts; |
|
+ for (counter = 0; counter < headers_in_array->nelts; counter++) { |
|
+ if (headers_in[counter].key == NULL |
|
+ || headers_in[counter].val == NULL |
|
+ |
|
+ /* Already sent */ |
|
+ || !strcasecmp(headers_in[counter].key, "Host") |
|
+ |
|
+ /* Clear out hop-by-hop request headers not to send |
|
+ * RFC2616 13.5.1 says we should strip these headers |
|
+ */ |
|
+ || !strcasecmp(headers_in[counter].key, "Keep-Alive") |
|
+ || !strcasecmp(headers_in[counter].key, "TE") |
|
+ || !strcasecmp(headers_in[counter].key, "Trailer") |
|
+ || !strcasecmp(headers_in[counter].key, "Upgrade") |
|
+ |
|
+ ) { |
|
+ continue; |
|
+ } |
|
+ /* Do we want to strip Proxy-Authorization ? |
|
+ * If we haven't used it, then NO |
|
+ * If we have used it then MAYBE: RFC2616 says we MAY propagate it. |
|
+ * So let's make it configurable by env. |
|
+ */ |
|
+ if (!strcasecmp(headers_in[counter].key,"Proxy-Authorization")) { |
|
+ if (r->user != NULL) { /* we've authenticated */ |
|
+ if (!apr_table_get(r->subprocess_env, "Proxy-Chain-Auth")) { |
|
+ continue; |
|
+ } |
|
+ } |
|
+ } |
|
+ |
|
+ /* Skip Transfer-Encoding and Content-Length for now. |
|
+ */ |
|
+ if (!strcasecmp(headers_in[counter].key, "Transfer-Encoding")) { |
|
+ *old_te_val = headers_in[counter].val; |
|
+ continue; |
|
+ } |
|
+ if (!strcasecmp(headers_in[counter].key, "Content-Length")) { |
|
+ *old_cl_val = headers_in[counter].val; |
|
+ continue; |
|
+ } |
|
+ |
|
+ /* for sub-requests, ignore freshness/expiry headers */ |
|
+ if (r->main) { |
|
+ if ( !strcasecmp(headers_in[counter].key, "If-Match") |
|
+ || !strcasecmp(headers_in[counter].key, "If-Modified-Since") |
|
+ || !strcasecmp(headers_in[counter].key, "If-Range") |
|
+ || !strcasecmp(headers_in[counter].key, "If-Unmodified-Since") |
|
+ || !strcasecmp(headers_in[counter].key, "If-None-Match")) { |
|
+ continue; |
|
+ } |
|
+ } |
|
+ |
|
+ buf = apr_pstrcat(p, headers_in[counter].key, ": ", |
|
+ headers_in[counter].val, CRLF, |
|
+ NULL); |
|
+ ap_xlate_proto_to_ascii(buf, strlen(buf)); |
|
+ e = apr_bucket_pool_create(buf, strlen(buf), p, c->bucket_alloc); |
|
+ APR_BRIGADE_INSERT_TAIL(header_brigade, e); |
|
+ } |
|
+ return OK; |
|
+} |
|
+ |
|
+PROXY_DECLARE(int) ap_proxy_pass_brigade(apr_bucket_alloc_t *bucket_alloc, |
|
+ request_rec *r, proxy_conn_rec *p_conn, |
|
+ conn_rec *origin, apr_bucket_brigade *bb, |
|
+ int flush) |
|
+{ |
|
+ apr_status_t status; |
|
+ apr_off_t transferred; |
|
+ |
|
+ if (flush) { |
|
+ apr_bucket *e = apr_bucket_flush_create(bucket_alloc); |
|
+ APR_BRIGADE_INSERT_TAIL(bb, e); |
|
+ } |
|
+ apr_brigade_length(bb, 0, &transferred); |
|
+ if (transferred != -1) |
|
+ p_conn->worker->s->transferred += transferred; |
|
+ status = ap_pass_brigade(origin->output_filters, bb); |
|
+ if (status != APR_SUCCESS) { |
|
+ ap_log_rerror(APLOG_MARK, APLOG_ERR, status, r, "AH01084: " |
|
+ "pass request body failed to %pI (%s)", |
|
+ p_conn->addr, p_conn->hostname); |
|
+ if (origin->aborted) { |
|
+ const char *ssl_note; |
|
+ |
|
+ if (((ssl_note = apr_table_get(origin->notes, "SSL_connect_rv")) |
|
+ != NULL) && (strcmp(ssl_note, "err") == 0)) { |
|
+ return ap_proxyerror(r, HTTP_INTERNAL_SERVER_ERROR, |
|
+ "Error during SSL Handshake with" |
|
+ " remote server"); |
|
+ } |
|
+ return APR_STATUS_IS_TIMEUP(status) ? HTTP_GATEWAY_TIME_OUT : HTTP_BAD_GATEWAY; |
|
+ } |
|
+ else { |
|
+ return HTTP_BAD_REQUEST; |
|
+ } |
|
+ } |
|
+ apr_brigade_cleanup(bb); |
|
+ return OK; |
|
+} |
|
+ |
|
+/* |
|
+ * Canonicalise http-like URLs. |
|
+ * scheme is the scheme for the URL |
|
+ * url is the URL starting with the first '/' |
|
+ * def_port is the default port for this scheme. |
|
+ */ |
|
+static int proxy_wstunnel_canon(request_rec *r, char *url) |
|
+{ |
|
+ char *host, *path, sport[7]; |
|
+ char *search = NULL; |
|
+ const char *err; |
|
+ char *scheme; |
|
+ apr_port_t port, def_port; |
|
+ |
|
+ /* ap_port_of_scheme() */ |
|
+ if (strncasecmp(url, "ws:", 3) == 0) { |
|
+ url += 3; |
|
+ scheme = "ws:"; |
|
+ def_port = apr_uri_port_of_scheme("http"); |
|
+ } |
|
+ else if (strncasecmp(url, "wss:", 4) == 0) { |
|
+ url += 4; |
|
+ scheme = "wss:"; |
|
+ def_port = apr_uri_port_of_scheme("https"); |
|
+ } |
|
+ else { |
|
+ return DECLINED; |
|
+ } |
|
+ |
|
+ port = def_port; |
|
+ ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "canonicalising URL %s", url); |
|
+ |
|
+ /* |
|
+ * do syntactic check. |
|
+ * We break the URL into host, port, path, search |
|
+ */ |
|
+ err = ap_proxy_canon_netloc(r->pool, &url, NULL, NULL, &host, &port); |
|
+ if (err) { |
|
+ ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "AH02439: " "error parsing URL %s: %s", |
|
+ url, err); |
|
+ return HTTP_BAD_REQUEST; |
|
+ } |
|
+ |
|
+ /* |
|
+ * now parse path/search args, according to rfc1738: |
|
+ * process the path. With proxy-nocanon set (by |
|
+ * mod_proxy) we use the raw, unparsed uri |
|
+ */ |
|
+ if (apr_table_get(r->notes, "proxy-nocanon")) { |
|
+ path = url; /* this is the raw path */ |
|
+ } |
|
+ else { |
|
+ path = ap_proxy_canonenc(r->pool, url, strlen(url), enc_path, 0, |
|
+ r->proxyreq); |
|
+ search = r->args; |
|
+ } |
|
+ if (path == NULL) |
|
+ return HTTP_BAD_REQUEST; |
|
+ |
|
+ apr_snprintf(sport, sizeof(sport), ":%d", port); |
|
+ |
|
+ if (ap_strchr_c(host, ':')) { |
|
+ /* if literal IPv6 address */ |
|
+ host = apr_pstrcat(r->pool, "[", host, "]", NULL); |
|
+ } |
|
+ r->filename = apr_pstrcat(r->pool, "proxy:", scheme, "//", host, sport, |
|
+ "/", path, (search) ? "?" : "", |
|
+ (search) ? search : "", NULL); |
|
+ return OK; |
|
+} |
|
+ |
|
+ |
|
+static int proxy_wstunnel_transfer(request_rec *r, conn_rec *c_i, conn_rec *c_o, |
|
+ apr_bucket_brigade *bb, char *name) |
|
+{ |
|
+ int rv; |
|
+#ifdef DEBUGGING |
|
+ apr_off_t len; |
|
+#endif |
|
+ |
|
+ do { |
|
+ apr_brigade_cleanup(bb); |
|
+ rv = ap_get_brigade(c_i->input_filters, bb, AP_MODE_READBYTES, |
|
+ APR_NONBLOCK_READ, AP_IOBUFSIZE); |
|
+ if (rv == APR_SUCCESS) { |
|
+ if (c_o->aborted) |
|
+ return APR_EPIPE; |
|
+ if (APR_BRIGADE_EMPTY(bb)) |
|
+ break; |
|
+#ifdef DEBUGGING |
|
+ len = -1; |
|
+ apr_brigade_length(bb, 0, &len); |
|
+ ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "AH02440: " |
|
+ "read %" APR_OFF_T_FMT |
|
+ " bytes from %s", len, name); |
|
+#endif |
|
+ rv = ap_pass_brigade(c_o->output_filters, bb); |
|
+ if (rv == APR_SUCCESS) { |
|
+ ap_fflush(c_o->output_filters, bb); |
|
+ } |
|
+ else { |
|
+ ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, "AH02441: " |
|
+ "error on %s - ap_pass_brigade", |
|
+ name); |
|
+ } |
|
+ } else if (!APR_STATUS_IS_EAGAIN(rv) && !APR_STATUS_IS_EOF(rv)) { |
|
+ ap_log_rerror(APLOG_MARK, APLOG_DEBUG, rv, r, "AH02442: " |
|
+ "error on %s - ap_get_brigade", |
|
+ name); |
|
+ } |
|
+ } while (rv == APR_SUCCESS); |
|
+ |
|
+ if (APR_STATUS_IS_EAGAIN(rv)) { |
|
+ rv = APR_SUCCESS; |
|
+ } |
|
+ return rv; |
|
+} |
|
+ |
|
+/* Search thru the input filters and remove the reqtimeout one */ |
|
+static void remove_reqtimeout(ap_filter_t *next) |
|
+{ |
|
+ ap_filter_t *reqto = NULL; |
|
+ ap_filter_rec_t *filter; |
|
+ |
|
+ filter = ap_get_input_filter_handle("reqtimeout"); |
|
+ if (!filter) { |
|
+ return; |
|
+ } |
|
+ |
|
+ while (next) { |
|
+ if (next->frec == filter) { |
|
+ reqto = next; |
|
+ break; |
|
+ } |
|
+ next = next->next; |
|
+ } |
|
+ if (reqto) { |
|
+ ap_remove_input_filter(reqto); |
|
+ } |
|
+} |
|
+ |
|
+/* |
|
+ * process the request and write the response. |
|
+ */ |
|
+static int ap_proxy_wstunnel_request(apr_pool_t *p, request_rec *r, |
|
+ proxy_conn_rec *conn, |
|
+ proxy_worker *worker, |
|
+ proxy_server_conf *conf, |
|
+ apr_uri_t *uri, |
|
+ char *url, char *server_portstr) |
|
+{ |
|
+ apr_status_t rv = APR_SUCCESS; |
|
+ apr_pollset_t *pollset; |
|
+ apr_pollfd_t pollfd; |
|
+ const apr_pollfd_t *signalled; |
|
+ apr_int32_t pollcnt, pi; |
|
+ apr_int16_t pollevent; |
|
+ conn_rec *c = r->connection; |
|
+ apr_socket_t *sock = conn->sock; |
|
+ conn_rec *backconn = conn->connection; |
|
+ int client_error = 0; |
|
+ char *buf; |
|
+ apr_bucket_brigade *header_brigade; |
|
+ apr_bucket *e; |
|
+ char *old_cl_val = NULL; |
|
+ char *old_te_val = NULL; |
|
+ apr_bucket_brigade *bb = apr_brigade_create(p, c->bucket_alloc); |
|
+ apr_socket_t *client_socket = ap_get_module_config(c->conn_config, &core_module); |
|
+ |
|
+ header_brigade = apr_brigade_create(p, backconn->bucket_alloc); |
|
+ |
|
+ ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "sending request"); |
|
+ |
|
+ rv = ap_proxy_create_hdrbrgd(p, header_brigade, r, conn, |
|
+ worker, conf, uri, url, server_portstr, |
|
+ &old_cl_val, &old_te_val); |
|
+ if (rv != OK) { |
|
+ return rv; |
|
+ } |
|
+ |
|
+ buf = apr_pstrcat(p, "Upgrade: WebSocket", CRLF, "Connection: Upgrade", CRLF, CRLF, NULL); |
|
+ ap_xlate_proto_to_ascii(buf, strlen(buf)); |
|
+ e = apr_bucket_pool_create(buf, strlen(buf), p, c->bucket_alloc); |
|
+ APR_BRIGADE_INSERT_TAIL(header_brigade, e); |
|
+ |
|
+ if ((rv = ap_proxy_pass_brigade(c->bucket_alloc, r, conn, backconn, |
|
+ header_brigade, 1)) != OK) |
|
+ return rv; |
|
+ |
|
+ ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "setting up poll()"); |
|
+ |
|
+ if ((rv = apr_pollset_create(&pollset, 2, p, 0)) != APR_SUCCESS) { |
|
+ ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, "AH02443: " |
|
+ "error apr_pollset_create()"); |
|
+ return HTTP_INTERNAL_SERVER_ERROR; |
|
+ } |
|
+ |
|
+#if 0 |
|
+ apr_socket_opt_set(sock, APR_SO_NONBLOCK, 1); |
|
+ apr_socket_opt_set(sock, APR_SO_KEEPALIVE, 1); |
|
+ apr_socket_opt_set(client_socket, APR_SO_NONBLOCK, 1); |
|
+ apr_socket_opt_set(client_socket, APR_SO_KEEPALIVE, 1); |
|
+#endif |
|
+ |
|
+ pollfd.p = p; |
|
+ pollfd.desc_type = APR_POLL_SOCKET; |
|
+ pollfd.reqevents = APR_POLLIN; |
|
+ pollfd.desc.s = sock; |
|
+ pollfd.client_data = NULL; |
|
+ apr_pollset_add(pollset, &pollfd); |
|
+ |
|
+ pollfd.desc.s = client_socket; |
|
+ apr_pollset_add(pollset, &pollfd); |
|
+ |
|
+ |
|
+ r->output_filters = c->output_filters; |
|
+ r->proto_output_filters = c->output_filters; |
|
+ r->input_filters = c->input_filters; |
|
+ r->proto_input_filters = c->input_filters; |
|
+ |
|
+ remove_reqtimeout(r->input_filters); |
|
+ |
|
+ while (1) { /* Infinite loop until error (one side closes the connection) */ |
|
+ if ((rv = apr_pollset_poll(pollset, -1, &pollcnt, &signalled)) |
|
+ != APR_SUCCESS) { |
|
+ if (APR_STATUS_IS_EINTR(rv)) { |
|
+ continue; |
|
+ } |
|
+ ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, "AH02444: " "error apr_poll()"); |
|
+ return HTTP_INTERNAL_SERVER_ERROR; |
|
+ } |
|
+ ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "AH02445: " |
|
+ "woke from poll(), i=%d", pollcnt); |
|
+ |
|
+ for (pi = 0; pi < pollcnt; pi++) { |
|
+ const apr_pollfd_t *cur = &signalled[pi]; |
|
+ |
|
+ if (cur->desc.s == sock) { |
|
+ pollevent = cur->rtnevents; |
|
+ if (pollevent & APR_POLLIN) { |
|
+ ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "AH02446: " |
|
+ "sock was readable"); |
|
+ rv = proxy_wstunnel_transfer(r, backconn, c, bb, "sock"); |
|
+ } |
|
+ else if ((pollevent & APR_POLLERR) |
|
+ || (pollevent & APR_POLLHUP)) { |
|
+ rv = APR_EPIPE; |
|
+ ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 0, r, "AH02447: " |
|
+ "err/hup on backconn"); |
|
+ } |
|
+ if (rv != APR_SUCCESS) |
|
+ client_error = 1; |
|
+ } |
|
+ else if (cur->desc.s == client_socket) { |
|
+ pollevent = cur->rtnevents; |
|
+ if (pollevent & APR_POLLIN) { |
|
+ ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "AH02448: " |
|
+ "client was readable"); |
|
+ rv = proxy_wstunnel_transfer(r, c, backconn, bb, "client"); |
|
+ } |
|
+ } |
|
+ else { |
|
+ rv = APR_EBADF; |
|
+ ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, "AH02449: " |
|
+ "unknown socket in pollset"); |
|
+ } |
|
+ |
|
+ } |
|
+ if (rv != APR_SUCCESS) { |
|
+ break; |
|
+ } |
|
+ } |
|
+ |
|
+ ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, |
|
+ "finished with poll() - cleaning up"); |
|
+ |
|
+ if (client_error) { |
|
+ return HTTP_INTERNAL_SERVER_ERROR; |
|
+ } |
|
+ return OK; |
|
+} |
|
+ |
|
+/* |
|
+ */ |
|
+static int proxy_wstunnel_handler(request_rec *r, proxy_worker *worker, |
|
+ proxy_server_conf *conf, |
|
+ char *url, const char *proxyname, |
|
+ apr_port_t proxyport) |
|
+{ |
|
+ int status; |
|
+ char server_portstr[32]; |
|
+ proxy_conn_rec *backend = NULL; |
|
+ char *scheme; |
|
+ int retry; |
|
+ int is_ssl = 0; |
|
+ conn_rec *c = r->connection; |
|
+ apr_pool_t *p = r->pool; |
|
+ apr_uri_t *uri; |
|
+ |
|
+ if (strncasecmp(url, "wss:", 4) == 0) { |
|
+ scheme = "WSS"; |
|
+ is_ssl = 1; |
|
+ } |
|
+ else if (strncasecmp(url, "ws:", 3) == 0) { |
|
+ scheme = "WS"; |
|
+ } |
|
+ else { |
|
+ ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "AH02450: " "declining URL %s", url); |
|
+ return DECLINED; |
|
+ } |
|
+ |
|
+ uri = apr_palloc(p, sizeof(*uri)); |
|
+ ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "AH02451: " "serving URL %s", url); |
|
+ |
|
+ /* create space for state information */ |
|
+ status = ap_proxy_acquire_connection(scheme, &backend, worker, |
|
+ r->server); |
|
+ if (status != OK) { |
|
+ if (backend) { |
|
+ backend->close = 1; |
|
+ ap_proxy_release_connection(scheme, backend, r->server); |
|
+ } |
|
+ return status; |
|
+ } |
|
+ |
|
+ backend->is_ssl = is_ssl; |
|
+ backend->close = 0; |
|
+ |
|
+ retry = 0; |
|
+ while (retry < 2) { |
|
+ char *locurl = url; |
|
+ /* Step One: Determine Who To Connect To */ |
|
+ status = ap_proxy_determine_connection(p, r, conf, worker, backend, |
|
+ uri, &locurl, proxyname, proxyport, |
|
+ server_portstr, |
|
+ sizeof(server_portstr)); |
|
+ |
|
+ if (status != OK) |
|
+ break; |
|
+ |
|
+ /* Step Two: Make the Connection */ |
|
+ if (ap_proxy_connect_backend(scheme, backend, worker, r->server)) { |
|
+ ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "AH02452: " |
|
+ "failed to make connection to backend: %s", |
|
+ backend->hostname); |
|
+ status = HTTP_SERVICE_UNAVAILABLE; |
|
+ break; |
|
+ } |
|
+ /* Step Three: Create conn_rec */ |
|
+ if (!backend->connection) { |
|
+ if ((status = ap_proxy_connection_create(scheme, backend, |
|
+ c, r->server)) != OK) |
|
+ break; |
|
+ } |
|
+ |
|
+ /* Step Three: Process the Request */ |
|
+ status = ap_proxy_wstunnel_request(p, r, backend, worker, conf, uri, locurl, |
|
+ server_portstr); |
|
+ break; |
|
+ } |
|
+ |
|
+ /* Do not close the socket */ |
|
+ ap_proxy_release_connection(scheme, backend, r->server); |
|
+ return status; |
|
+} |
|
+ |
|
+static void ap_proxy_http_register_hook(apr_pool_t *p) |
|
+{ |
|
+ proxy_hook_scheme_handler(proxy_wstunnel_handler, NULL, NULL, APR_HOOK_FIRST); |
|
+ proxy_hook_canon_handler(proxy_wstunnel_canon, NULL, NULL, APR_HOOK_FIRST); |
|
+} |
|
+ |
|
+APLOG_USE_MODULE(proxy_wstunnel); |
|
+module AP_MODULE_DECLARE_DATA proxy_wstunnel_module = { |
|
+ STANDARD20_MODULE_STUFF, |
|
+ NULL, /* create per-directory config structure */ |
|
+ NULL, /* merge per-directory config structures */ |
|
+ NULL, /* create per-server config structure */ |
|
+ NULL, /* merge per-server config structures */ |
|
+ NULL, /* command apr_table_t */ |
|
+ ap_proxy_http_register_hook /* register hooks */ |
|
+}; |
|
diff -Naur a/modules/proxy/mod_proxy_wstunnel.dsp b/modules/proxy/mod_proxy_wstunnel.dsp |
|
--- a/modules/proxy/mod_proxy_wstunnel.dsp |
|
+++ b/modules/proxy/mod_proxy_wstunnel.dsp |
|
@@ -0,0 +1,128 @@ |
|
+# Microsoft Developer Studio Project File - Name="mod_proxy_wstunnel" - Package Owner=<4> |
|
+# Microsoft Developer Studio Generated Build File, Format Version 6.00 |
|
+# ** DO NOT EDIT ** |
|
+ |
|
+# TARGTYPE "Win32 (x86) Dynamic-Link Library" 0x0102 |
|
+ |
|
+CFG=mod_proxy_wstunnel - Win32 Release |
|
+!MESSAGE This is not a valid makefile. To build this project using NMAKE, |
|
+!MESSAGE use the Export Makefile command and run |
|
+!MESSAGE |
|
+!MESSAGE NMAKE /f "mod_proxy_wstunnel.mak". |
|
+!MESSAGE |
|
+!MESSAGE You can specify a configuration when running NMAKE |
|
+!MESSAGE by defining the macro CFG on the command line. For example: |
|
+!MESSAGE |
|
+!MESSAGE NMAKE /f "mod_proxy_wstunnel.mak" CFG="mod_proxy_wstunnel - Win32 Release" |
|
+!MESSAGE |
|
+!MESSAGE Possible choices for configuration are: |
|
+!MESSAGE |
|
+!MESSAGE "mod_proxy_wstunnel - Win32 Release" (based on "Win32 (x86) Dynamic-Link Library") |
|
+!MESSAGE "mod_proxy_wstunnel - Win32 Debug" (based on "Win32 (x86) Dynamic-Link Library") |
|
+!MESSAGE |
|
+ |
|
+# Begin Project |
|
+# PROP AllowPerConfigDependencies 0 |
|
+# PROP Scc_ProjName "" |
|
+# PROP Scc_LocalPath "" |
|
+CPP=cl.exe |
|
+MTL=midl.exe |
|
+RSC=rc.exe |
|
+ |
|
+!IF "$(CFG)" == "mod_proxy_wstunnel - Win32 Release" |
|
+ |
|
+# PROP BASE Use_MFC 0 |
|
+# PROP BASE Use_Debug_Libraries 0 |
|
+# PROP BASE Output_Dir "Release" |
|
+# PROP BASE Intermediate_Dir "Release" |
|
+# PROP BASE Target_Dir "" |
|
+# PROP Use_MFC 0 |
|
+# PROP Use_Debug_Libraries 0 |
|
+# PROP Output_Dir "Release" |
|
+# PROP Intermediate_Dir "Release" |
|
+# PROP Ignore_Export_Lib 0 |
|
+# PROP Target_Dir "" |
|
+# ADD BASE CPP /nologo /MD /W3 /O2 /D "WIN32" /D "NDEBUG" /D "_WINDOWS" /FD /c |
|
+# ADD CPP /nologo /MD /W3 /O2 /Oy- /Zi /I "../../include" /I "../../srclib/apr/include" /I "../../srclib/apr-util/include" /D "NDEBUG" /D "WIN32" /D "_WINDOWS" /Fd"Release\mod_proxy_wstunnel_src" /FD /c |
|
+# ADD BASE MTL /nologo /D "NDEBUG" /win32 |
|
+# ADD MTL /nologo /D "NDEBUG" /mktyplib203 /win32 |
|
+# ADD BASE RSC /l 0x809 /d "NDEBUG" |
|
+# ADD RSC /l 0x409 /fo"Release/mod_proxy_wstunnel.res" /i "../../include" /i "../../srclib/apr/include" /d "NDEBUG" /d BIN_NAME="mod_proxy_wstunnel.so" /d LONG_NAME="proxy_wstunnel_module for Apache" |
|
+BSC32=bscmake.exe |
|
+# ADD BASE BSC32 /nologo |
|
+# ADD BSC32 /nologo |
|
+LINK32=link.exe |
|
+# ADD BASE LINK32 kernel32.lib ws2_32.lib mswsock.lib /nologo /subsystem:windows /dll /out:".\Release\mod_proxy_wstunnel.so" /base:@..\..\os\win32\BaseAddr.ref,mod_proxy_wstunnel.so |
|
+# ADD LINK32 kernel32.lib ws2_32.lib mswsock.lib /nologo /subsystem:windows /dll /incremental:no /debug /out:".\Release\mod_proxy_wstunnel.so" /base:@..\..\os\win32\BaseAddr.ref,mod_proxy_wstunnel.so /opt:ref |
|
+# Begin Special Build Tool |
|
+TargetPath=.\Release\mod_proxy_wstunnel.so |
|
+SOURCE="$(InputPath)" |
|
+PostBuild_Desc=Embed .manifest |
|
+PostBuild_Cmds=if exist $(TargetPath).manifest mt.exe -manifest $(TargetPath).manifest -outputresource:$(TargetPath);2 |
|
+# End Special Build Tool |
|
+ |
|
+!ELSEIF "$(CFG)" == "mod_proxy_wstunnel - Win32 Debug" |
|
+ |
|
+# PROP BASE Use_MFC 0 |
|
+# PROP BASE Use_Debug_Libraries 1 |
|
+# PROP BASE Output_Dir "Debug" |
|
+# PROP BASE Intermediate_Dir "Debug" |
|
+# PROP BASE Target_Dir "" |
|
+# PROP Use_MFC 0 |
|
+# PROP Use_Debug_Libraries 1 |
|
+# PROP Output_Dir "Debug" |
|
+# PROP Intermediate_Dir "Debug" |
|
+# PROP Ignore_Export_Lib 0 |
|
+# PROP Target_Dir "" |
|
+# ADD BASE CPP /nologo /MDd /W3 /EHsc /Zi /Od /D "WIN32" /D "_DEBUG" /D "_WINDOWS" /FD /c |
|
+# ADD CPP /nologo /MDd /W3 /EHsc /Zi /Od /I "../../include" /I "../../srclib/apr/include" /I "../../srclib/apr-util/include" /D "_DEBUG" /D "WIN32" /D "_WINDOWS" /Fd"Debug\mod_proxy_wstunnel_src" /FD /c |
|
+# ADD BASE MTL /nologo /D "_DEBUG" /win32 |
|
+# ADD MTL /nologo /D "_DEBUG" /mktyplib203 /win32 |
|
+# ADD BASE RSC /l 0x809 /d "_DEBUG" |
|
+# ADD RSC /l 0x409 /fo"Debug/mod_proxy_wstunnel.res" /i "../../include" /i "../../srclib/apr/include" /d "_DEBUG" /d BIN_NAME="mod_proxy_wstunnel.so" /d LONG_NAME="proxy_wstunnel_module for Apache" |
|
+BSC32=bscmake.exe |
|
+# ADD BASE BSC32 /nologo |
|
+# ADD BSC32 /nologo |
|
+LINK32=link.exe |
|
+# ADD BASE LINK32 kernel32.lib ws2_32.lib mswsock.lib /nologo /subsystem:windows /dll /incremental:no /debug /out:".\Debug\mod_proxy_wstunnel.so" /base:@..\..\os\win32\BaseAddr.ref,mod_proxy_wstunnel.so |
|
+# ADD LINK32 kernel32.lib ws2_32.lib mswsock.lib /nologo /subsystem:windows /dll /incremental:no /debug /out:".\Debug\mod_proxy_wstunnel.so" /base:@..\..\os\win32\BaseAddr.ref,mod_proxy_wstunnel.so |
|
+# Begin Special Build Tool |
|
+TargetPath=.\Debug\mod_proxy_wstunnel.so |
|
+SOURCE="$(InputPath)" |
|
+PostBuild_Desc=Embed .manifest |
|
+PostBuild_Cmds=if exist $(TargetPath).manifest mt.exe -manifest $(TargetPath).manifest -outputresource:$(TargetPath);2 |
|
+# End Special Build Tool |
|
+ |
|
+!ENDIF |
|
+ |
|
+# Begin Target |
|
+ |
|
+# Name "mod_proxy_wstunnel - Win32 Release" |
|
+# Name "mod_proxy_wstunnel - Win32 Debug" |
|
+# Begin Group "Source Files" |
|
+ |
|
+# PROP Default_Filter "cpp;c;cxx;rc;def;r;odl;hpj;bat;for;f90" |
|
+# Begin Source File |
|
+ |
|
+SOURCE=.\mod_proxy_wstunnel.c |
|
+# End Source File |
|
+# End Group |
|
+# Begin Group "Header Files" |
|
+ |
|
+# PROP Default_Filter ".h" |
|
+# Begin Source File |
|
+ |
|
+SOURCE=.\mod_proxy.h |
|
+# End Source File |
|
+# End Group |
|
+# Begin Source File |
|
+ |
|
+SOURCE=.\mod_proxy_wstunnel.h |
|
+# End Source File |
|
+# End Group |
|
+# Begin Source File |
|
+ |
|
+SOURCE=..\..\build\win32\httpd.rc |
|
+# End Source File |
|
+# End Target |
|
+# End Project |