#pike __REAL_VERSION__ |
|
|
constant HTTP_CONTINUE = 100; |
constant HTTP_SWITCH_PROT = 101; |
constant DAV_PROCESSING = 102; |
|
|
constant HTTP_OK = 200; |
constant HTTP_CREATED = 201; |
constant HTTP_ACCEPTED = 202; |
constant HTTP_NONAUTHORATIVE = 203; |
constant HTTP_NO_CONTENT = 204; |
constant HTTP_RESET_CONTENT = 205; |
constant HTTP_PARTIAL_CONTENT = 206; |
constant DAV_MULTISTATUS = 207; |
constant DELTA_HTTP_IM_USED = 226; |
|
|
constant HTTP_MULTIPLE = 300; |
constant HTTP_MOVED_PERM = 301; |
constant HTTP_FOUND = 302; |
constant HTTP_SEE_OTHER = 303; |
constant HTTP_NOT_MODIFIED = 304; |
constant HTTP_USE_PROXY = 305; |
|
constant HTTP_TEMP_REDIRECT = 307; |
|
|
constant HTTP_BAD = 400; |
constant HTTP_UNAUTH = 401; |
constant HTTP_PAY = 402; |
constant HTTP_FORBIDDEN = 403; |
constant HTTP_NOT_FOUND = 404; |
constant HTTP_METHOD_INVALID = 405; |
constant HTTP_NOT_ACCEPTABLE = 406; |
constant HTTP_PROXY_AUTH_REQ = 407; |
constant HTTP_TIMEOUT = 408; |
constant HTTP_CONFLICT = 409; |
constant HTTP_GONE = 410; |
constant HTTP_LENGTH_REQ = 411; |
constant HTTP_PRECOND_FAILED = 412; |
constant HTTP_REQ_TOO_LARGE = 413; |
constant HTTP_URI_TOO_LONG = 414; |
constant HTTP_UNSUPP_MEDIA = 415; |
constant HTTP_BAD_RANGE = 416; |
constant HTTP_EXPECT_FAILED = 417; |
constant HTCPCP_TEAPOT = 418; |
constant DAV_UNPROCESSABLE = 422; |
constant DAV_LOCKED = 423; |
constant DAV_FAILED_DEP = 424; |
|
constant HTTP_LEGALLY_RESTRICTED= 451; |
|
|
constant HTTP_INTERNAL_ERR = 500; |
constant HTTP_NOT_IMPL = 501; |
constant HTTP_BAD_GW = 502; |
constant HTTP_UNAVAIL = 503; |
constant HTTP_GW_TIMEOUT = 504; |
constant HTTP_UNSUPP_VERSION = 505; |
constant TCN_VARIANT_NEGOTIATES = 506; |
constant DAV_STORAGE_FULL = 507; |
|
constant response_codes = |
([ |
|
100:"100 Continue", |
101:"101 Switching Protocols", |
102:"102 Processing", |
103:"103 Checkpoint", |
122:"122 Request-URI too long", |
|
|
200:"200 OK", |
201:"201 Created, URI follows", |
202:"202 Accepted", |
203:"203 Non-Authoritative Information", |
204:"204 No Content", |
205:"205 Reset Content", |
206:"206 Partial Content", |
207:"207 Multi-Status", |
226:"226 IM Used", |
|
|
300:"300 Moved", |
301:"301 Permanent Relocation", |
302:"302 Found", |
303:"303 See Other", |
304:"304 Not Modified", |
305:"305 Use Proxy", |
306:"306 Switch Proxy", |
307:"307 Temporary Redirect", |
308:"308 Resume Incomplete", |
|
|
400:"400 Bad Request", |
401:"401 Access denied", |
402:"402 Payment Required", |
403:"403 Forbidden", |
404:"404 No such file or directory.", |
405:"405 Method not allowed", |
406:"406 Not Acceptable", |
407:"407 Proxy authorization needed", |
408:"408 Request timeout", |
409:"409 Conflict", |
410:"410 Gone", |
411:"411 Length Required", |
412:"412 Precondition Failed", |
413:"413 Request Entity Too Large", |
414:"414 Request-URI Too Large", |
415:"415 Unsupported Media Type", |
416:"416 Requested range not statisfiable", |
417:"417 Expectation Failed", |
418:"418 I'm a teapot", |
422:"422 Unprocessable Entity", |
423:"423 Locked", |
424:"424 Failed Dependency", |
425:"425 Unordered Collection", |
426:"426 Upgrade Required", |
451:"451 Unavailable for Legal Reasons", |
|
|
500:"500 Internal Server Error.", |
501:"501 Not Implemented", |
502:"502 Bad Gateway", |
503:"503 Service unavailable", |
504:"504 Gateway Timeout", |
505:"505 HTTP Version Not Supported", |
506:"506 Variant Also Negotiates", |
507:"507 Insufficient Storage", |
509:"509 Bandwidth Limit Exceeded", |
510:"510 Not Extended", |
598:"598 Network read timeout error", |
599:"599 Network connect timeout error", |
]); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.Query do_proxied_method(string|Standards.URI proxy, |
string user, string password, |
string method, |
string|Standards.URI url, |
void|mapping(string:int|string|array(string)) query_variables, |
void|mapping(string:string|array(string)) request_headers, |
void|Protocols.HTTP.Query con, void|string data) |
{ |
if (!proxy || (proxy == "")) { |
return do_method(method, url, query_variables, request_headers, con, data); |
} |
|
|
proxy = Standards.URI(proxy); |
url = Standards.URI(url); |
|
mapping(string:string|array(string)) proxy_headers; |
|
if( user || password ) |
{ |
if( !request_headers ) |
proxy_headers = ([]); |
else |
proxy_headers = request_headers + ([]); |
|
proxy_headers["Proxy-Authorization"] = "Basic " |
+ MIME.encode_base64((user || "") + ":" + (password || "")); |
} |
|
if (url->scheme == "http") { |
if( query_variables ) |
url->set_query_variables( url->get_query_variables() + |
query_variables ); |
string web_url = (string)url; |
|
|
url->host = proxy->host; |
url->port = proxy->port; |
query_variables = url->query = 0; |
url->path = web_url; |
#if constant(SSL.sslfile) |
} else if (url->scheme == "https") { |
#ifdef HTTP_QUERY_DEBUG |
werror("Proxied SSL request.\n"); |
#endif |
if (!con || (con->host != url->host) || (con->port != url->port)) { |
|
|
proxy->path = url->host + ":" + url->port; |
if (!proxy_headers) proxy_headers = ([]); |
proxy_headers->connection = "keep-alive"; |
m_delete(proxy_headers, "authorization"); |
con = do_method("CONNECT", proxy, 0, proxy_headers); |
con->data(0); |
if (con->status/100 > 2) { |
return con; |
} |
con->headers["connection"] = "keep-alive"; |
con->headers["content-length"] = "0"; |
con->host = url->host; |
con->port = url->port; |
con->https = 1; |
con->start_tls(1); |
} |
proxy_headers = request_headers; |
#endif |
} else { |
error("Can't handle proxying of %O.\n", url->scheme); |
} |
|
return do_method(method, url, query_variables, proxy_headers, con, data); |
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.Query do_method(string method, |
string|Standards.URI url, |
void|mapping(string:int|string|array(string)) query_variables, |
void|mapping(string:string|array(string)) request_headers, |
void|Protocols.HTTP.Query con, void|string data) |
{ |
if(stringp(url)) |
url=Standards.URI(url); |
|
if( (< "httpu", "httpmu" >)[url->scheme] ) { |
return do_udp_method(method, url, query_variables, request_headers, |
con, data); |
} |
|
if(!con) |
con = .Query(); |
|
#if constant(SSL.sslfile) |
if(url->scheme!="http" && url->scheme!="https") |
error("Can't handle %O or any other protocols than HTTP or HTTPS.\n", |
url->scheme); |
|
con->https = (url->scheme=="https")? 1 : 0; |
#else |
if(url->scheme!="http") |
error("Can't handle %O or any other protocol than HTTP " |
"(HTTPS requires Nettle support).\n", |
url->scheme); |
#endif |
|
mapping default_headers = ([ |
"user-agent" : "Mozilla/5.0 (compatible; MSIE 6.0; Pike HTTP client)" |
" Pike/" + __REAL_MAJOR__ + "." + __REAL_MINOR__ + "." + __REAL_BUILD__, |
"host" : url->host + |
(url->port!=(url->scheme=="https"?443:80)?":"+url->port:"")]); |
|
if(url->user || url->password) |
default_headers->authorization = "Basic " |
+ MIME.encode_base64(url->user + ":" + |
(url->password || "")); |
|
if(!request_headers) |
request_headers = default_headers; |
else |
request_headers = default_headers | |
mkmapping(lower_case(indices(request_headers)[*]), |
values(request_headers)); |
|
string query=url->query; |
if(query_variables && sizeof(query_variables)) |
{ |
if(query) |
query+="&"+http_encode_query(query_variables); |
else |
query=http_encode_query(query_variables); |
} |
|
string path=url->path; |
if(path=="") path="/"; |
|
con->sync_request(url->host,url->port, |
method+" "+path+(query?("?"+query):"")+" HTTP/1.0", |
request_headers, data); |
|
if (!con->ok) { |
if (con->errno) |
error ("I/O error: %s\n", strerror (con->errno)); |
return 0; |
} |
return con; |
} |
|
protected .Query do_udp_method(string method, Standards.URI url, |
void|mapping(string:int|string|array(string)) query_variables, |
void|mapping(string:string|array(string)) |
request_headers, void|Protocols.HTTP.Query con, |
void|string data) |
{ |
if(!request_headers) |
request_headers = ([]); |
|
string path = url->path; |
if(path=="") { |
if(url->method=="httpmu") |
path = "*"; |
else |
path = "/"; |
} |
string msg = method + " " + path + " HTTP/1.1\r\n"; |
|
Stdio.UDP udp = Stdio.UDP(); |
int port = 10000 + random(1000); |
int i; |
while(1) { |
if( !catch( udp->bind(port++, 0, 1) ) ) break; |
if( i++ > 1000 ) error("Could not open a UDP port.\n"); |
} |
if(url->method=="httpmu") { |
mapping ifs = Stdio.gethostip(); |
if(!sizeof(ifs)) error("No Internet interface found.\n"); |
foreach(ifs; string i; mapping data) |
if(sizeof(data->ips)) { |
udp->enable_multicast(data->ips[0]); |
break; |
} |
udp->add_membership(url->host, 0, 0); |
} |
udp->set_multicast_ttl(4); |
udp->send(url->host, url->port, msg); |
if (!con) { |
con = .Query(); |
} |
con->con = udp; |
return con; |
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void do_async_method(string method, |
string|Standards.URI url, |
void|mapping(string:int|string|array(string)) query_variables, |
void|mapping(string:string|array(string)) request_headers, |
Protocols.HTTP.Query con, void|string data) |
{ |
if(stringp(url)) |
url=Standards.URI(url); |
|
if( (< "httpu", "httpmu" >)[url->scheme] ) { |
error("Asynchronous httpu or httpmu not yet supported.\n"); |
} |
|
#if constant(SSL.sslfile) |
if(url->scheme!="http" && url->scheme!="https") |
error("Can't handle %O or any other protocols than HTTP or HTTPS.\n", |
url->scheme); |
|
con->https = (url->scheme=="https")? 1 : 0; |
#else |
if(url->scheme!="http") |
error("Can't handle %O or any other protocol than HTTP.\n", |
url->scheme); |
#endif |
|
if(!request_headers) |
request_headers = ([]); |
mapping default_headers = ([ |
"user-agent" : "Mozilla/5.0 (compatible; MSIE 6.0; Pike HTTP client)" |
" Pike/" + __REAL_MAJOR__ + "." + __REAL_MINOR__ + "." + __REAL_BUILD__, |
"host" : url->host + |
(url->port!=(url->scheme=="https"?443:80)?":"+url->port:"")]); |
|
if(url->user || url->passwd) |
default_headers->authorization = "Basic " |
+ MIME.encode_base64(url->user + ":" + |
(url->password || "")); |
request_headers = default_headers | request_headers; |
|
string query=url->query; |
if(query_variables && sizeof(query_variables)) |
{ |
if(query) |
query+="&"+http_encode_query(query_variables); |
else |
query=http_encode_query(query_variables); |
} |
|
string path=url->path; |
if(path=="") path="/"; |
|
if (!con->headers || |
lower_case(con->headers["connection"]||"close") == "close") { |
|
con->data_timeout = 120; |
con->timeout = 120; |
con->con = 0; |
|
} |
|
con->async_request(url->host, url->port, |
method+" "+path+(query?("?"+query):"")+" HTTP/1.0", |
request_headers, data); |
} |
|
protected void https_proxy_connect_fail(Protocols.HTTP.Query con, |
array(mixed) orig_cb_info, |
Standards.URI url, string method, |
mapping(string:string) query_variables, |
mapping(string:string) request_headers, |
string data) |
{ |
con->set_callbacks(@orig_cb_info); |
con->request_fail(con, @con->extra_args); |
} |
|
protected void https_proxy_connect_ok(Protocols.HTTP.Query con, |
array(mixed) orig_cb_info, |
Standards.URI url, string method, |
mapping(string:string) query_variables, |
mapping(string:string) request_headers, |
string data) |
{ |
con->set_callbacks(@orig_cb_info); |
|
con->con->set_nonblocking(0, |
lambda() { |
do_async_method(method, url, query_variables, |
request_headers, con, data); |
}, con->async_failed); |
|
con->headers["connection"] = "keep-alive"; |
con->headers["content-length"] = "0"; |
con->host = url->host; |
con->port = url->port; |
con->https = 1; |
con->start_tls(0); |
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void do_async_proxied_method(string|Standards.URI proxy, |
string user, string password, |
string method, |
string|Standards.URI url, |
void|mapping(string:int|string|array(string)) query_variables, |
void|mapping(string:string|array(string)) request_headers, |
Protocols.HTTP.Query con, void|string data) |
{ |
if (!proxy || (proxy == "")) { |
do_async_method(method, url, query_variables, request_headers, con, data); |
return; |
} |
|
|
proxy = Standards.URI(proxy); |
url = Standards.URI(url); |
|
if( (< "httpu", "httpmu" >)[url->scheme] ) { |
error("Asynchronous httpu or httpmu not yet supported.\n"); |
} |
|
mapping(string:string|array(string)) proxy_headers; |
|
if( user || password ) |
{ |
if( !request_headers ) |
proxy_headers = ([]); |
else |
proxy_headers = request_headers + ([]); |
|
proxy_headers["Proxy-Authorization"] = "Basic " |
+ MIME.encode_base64((user || "") + ":" + (password || "")); |
} |
|
if (url->scheme == "http") { |
if( query_variables ) |
url->set_query_variables( url->get_query_variables() + |
query_variables ); |
string web_url = (string)url; |
|
|
url->host = proxy->host; |
url->port = proxy->port; |
query_variables = url->query = 0; |
url->path = web_url; |
#if constant(SSL.sslfile) |
} else if(url->scheme == "https") { |
#ifdef HTTP_QUERY_DEBUG |
werror("Proxied SSL request.\n"); |
#endif |
if (!con || (con->host != url->host) || (con->port != url->port)) { |
|
|
proxy->path = url->host + ":" + url->port; |
if (!proxy_headers) proxy_headers = ([]); |
proxy_headers->connection = "keep-alive"; |
m_delete(proxy_headers, "authorization"); |
|
array(mixed) orig_cb_info = ({ |
con->request_ok, |
con->request_fail, |
@con->extra_args, |
}); |
con->set_callbacks(https_proxy_connect_ok, |
https_proxy_connect_fail, |
orig_cb_info, |
url, method, |
query_variables, |
request_headers && request_headers + ([]), |
data); |
method = "CONNECT"; |
url = proxy; |
data = 0; |
} else { |
proxy_headers = request_headers; |
} |
#endif |
} else { |
error("Can't handle proxying of %O.\n", url->scheme); |
} |
|
do_async_method(method, url, query_variables, proxy_headers, con, data); |
} |
|
|
|
|
|
|
|
|
.Query get_url(string|Standards.URI url, |
void|mapping(string:int|string|array(string)) query_variables, |
void|mapping(string:string|array(string)) request_headers, |
void|Protocols.HTTP.Query con) |
{ |
return do_method("GET", url, query_variables, request_headers, con); |
} |
|
|
|
|
|
|
|
|
.Query put_url(string|Standards.URI url, |
void|string file, |
void|mapping(string:int|string|array(string)) query_variables, |
void|mapping(string:string|array(string)) request_headers, |
void|Protocols.HTTP.Query con) |
{ |
return do_method("PUT", url, query_variables, request_headers, con, file); |
} |
|
|
|
|
|
|
|
|
.Query delete_url(string|Standards.URI url, |
void|mapping(string:int|string|array(string)) query_variables, |
void|mapping(string:string|array(string)) request_headers, |
void|Protocols.HTTP.Query con) |
{ |
return do_method("DELETE", url, query_variables, request_headers, con); |
} |
|
|
|
|
|
array(string) get_url_nice(string|Standards.URI url, |
void|mapping(string:int|string|array(string)) query_variables, |
void|mapping(string:string|array(string)) request_headers, |
void|Protocols.HTTP.Query con) |
{ |
.Query c; |
multiset seen = (<>); |
do { |
if(!url) return 0; |
if(seen[url] || sizeof(seen)>1000) return 0; |
seen[url]=1; |
c = get_url(url, query_variables, request_headers, con); |
if(!c) return 0; |
if(c->status==302) |
url = Standards.URI(c->headers->location, url); |
} while( c->status!=200 ); |
return ({ c->headers["content-type"], c->data() }); |
} |
|
|
|
|
|
string get_url_data(string|Standards.URI url, |
void|mapping(string:int|string|array(string)) query_variables, |
void|mapping(string:string|array(string)) request_headers, |
void|Protocols.HTTP.Query con) |
{ |
array(string) z = get_url_nice(url, query_variables, request_headers, con); |
return z && z[1]; |
} |
|
|
|
|
|
|
.Query post_url(string|Standards.URI url, |
mapping(string:int|string|array(string))|string query_variables, |
void|mapping(string:string|array(string)) request_headers, |
void|Protocols.HTTP.Query con) |
{ |
return do_method("POST", url, 0, stringp(query_variables) ? request_headers |
: (request_headers||([]))| |
(["content-type": |
"application/x-www-form-urlencoded"]), |
con, |
stringp(query_variables) ? query_variables |
: http_encode_query(query_variables)); |
} |
|
|
|
array(string) post_url_nice(string|Standards.URI url, |
mapping(string:int|string|array(string))|string query_variables, |
void|mapping(string:string|array(string)) request_headers, |
void|Protocols.HTTP.Query con) |
{ |
.Query c = post_url(url, query_variables, request_headers, con); |
return c && ({ c->headers["content-type"], c->data() }); |
} |
|
|
|
string post_url_data(string|Standards.URI url, |
mapping(string:int|string|array(string))|string query_variables, |
void|mapping(string:string|array(string)) request_headers, |
void|Protocols.HTTP.Query con) |
{ |
.Query z = post_url(url, query_variables, request_headers, con); |
return z && z->data(); |
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
string http_encode_query(mapping(string:int|string|array(string)) variables) |
{ |
return Array.map((array)variables, |
lambda(array(string|int|array(string)) v) |
{ |
if (intp(v[1])) |
return uri_encode(v[0]); |
if (arrayp(v[1])) |
return map(v[1], lambda (string val) { |
return |
uri_encode(v[0])+"="+ |
uri_encode(val); |
})*"&"; |
return uri_encode(v[0])+"="+ uri_encode(v[1]); |
})*"&"; |
} |
|
protected local constant gen_delims = ":/?#[]@" |
|
|
"%"; |
|
protected local constant sub_delims = "!$&'()*+,;="; |
|
|
protected local constant other_chars = |
(string) enumerate (0x20) + "\x7f" |
" \"<>\\^`{|}"; |
|
protected local constant eight_bit_chars = (string) enumerate (0x80, 1, 0x80); |
|
string percent_encode (string s) |
|
|
|
|
|
|
|
|
|
|
|
|
{ |
constant replace_chars = (gen_delims + sub_delims + |
other_chars + eight_bit_chars); |
return replace (s, |
|
|
sprintf ("%c", ((array(int)) replace_chars)[*]), |
|
|
|
sprintf ("%%%02X", ((array(int)) replace_chars)[*])); |
} |
|
string percent_decode (string s) |
|
|
|
|
|
|
|
|
{ |
return _Roxen.http_decode_string (s); |
} |
|
string uri_encode (string s) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{ |
return percent_encode (string_to_utf8 (s)); |
} |
|
string uri_encode_invalids (string s) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{ |
constant replace_chars = other_chars + eight_bit_chars; |
return replace (string_to_utf8 (s), |
sprintf ("%c", ((array(int)) replace_chars)[*]), |
sprintf ("%%%02X", ((array(int)) replace_chars)[*])); |
} |
|
string uri_decode (string s) |
|
|
|
|
|
|
{ |
|
|
|
|
return utf8_to_string (_Roxen.http_decode_string (s)); |
} |
|
string iri_encode (string s) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{ |
constant replace_chars = gen_delims + sub_delims + other_chars; |
return replace (s, |
sprintf ("%c", ((array(int)) replace_chars)[*]), |
sprintf ("%%%02X", ((array(int)) replace_chars)[*])); |
} |
|
#if 0 |
|
|
|
string uri_normalize (string s) |
|
|
|
|
|
|
|
|
|
|
{ |
|
} |
|
string iri_normalize (string s) |
|
|
|
|
|
|
|
|
|
|
{ |
|
} |
|
#endif |
|
string quoted_string_encode (string s) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{ |
return replace (s, (["\"": "\\\"", "\\": "\\\\"])); |
} |
|
string quoted_string_decode (string s) |
|
|
|
|
|
|
{ |
return map (s / "\\\\", replace, "\\", "") * "\\"; |
} |
|
|
|
__deprecated__ string http_encode_string(string in) |
|
|
|
|
|
|
|
|
|
|
|
|
{ |
return uri_encode (in); |
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
__deprecated__ string http_encode_cookie(string f) |
{ |
return replace( |
f, |
({ "\000", "\001", "\002", "\003", "\004", "\005", "\006", "\007", |
"\010", "\011", "\012", "\013", "\014", "\015", "\016", "\017", |
"\020", "\021", "\022", "\023", "\024", "\025", "\026", "\027", |
"\030", "\031", "\032", "\033", "\034", "\035", "\036", "\037", |
"\177", |
"\200", "\201", "\202", "\203", "\204", "\205", "\206", "\207", |
"\210", "\211", "\212", "\213", "\214", "\215", "\216", "\217", |
"\220", "\221", "\222", "\223", "\224", "\225", "\226", "\227", |
"\230", "\231", "\232", "\233", "\234", "\235", "\236", "\237", |
" ", "%", "'", "\"", ",", ";", "=", ":" }), |
({ |
"%00", "%01", "%02", "%03", "%04", "%05", "%06", "%07", |
"%08", "%09", "%0a", "%0b", "%0c", "%0d", "%0e", "%0f", |
"%10", "%11", "%12", "%13", "%14", "%15", "%16", "%17", |
"%18", "%19", "%1a", "%1b", "%1c", "%1d", "%1e", "%1f", |
"%7f", |
"%80", "%81", "%82", "%83", "%84", "%85", "%86", "%87", |
"%88", "%89", "%8a", "%8b", "%8c", "%8d", "%8e", "%8f", |
"%90", "%91", "%92", "%93", "%94", "%95", "%96", "%97", |
"%98", "%99", "%9a", "%9b", "%9c", "%9d", "%9e", "%9f", |
"%20", "%25", "%27", "%22", "%2c", "%3b", "%3d", "%3a" })); |
} |
|
|
|
|
__deprecated__ string unentity(string s) |
{ |
return master()->resolv("Parser.parse_html_entities")(s,1); |
} |
|
|