mirror of
git://git.psyced.org/git/psyced
synced 2024-08-15 03:25:10 +00:00
1166 lines
37 KiB
OpenEdge ABL
1166 lines
37 KiB
OpenEdge ABL
// vim:foldmethod=marker:syntax=lpc:noexpandtab
|
|
// $Id: parse.i,v 1.345 2008/04/15 19:36:44 lynx Exp $
|
|
//
|
|
#ifndef FORK
|
|
|
|
#ifndef __PIKE__
|
|
# include <tls.h>
|
|
#endif
|
|
|
|
// PSYC MESSAGE PARSER - parses both MMP and PSYC the old way
|
|
//
|
|
// THIS IS THE ORIGINAL LYNXISH PSYC PARSER
|
|
|
|
// this flag should enable forward-checks of dns resolutions..
|
|
// currently we don't have that, so this flag actually disables
|
|
// use of unprooven resolved hostnames and reduces everything to
|
|
// ip numbers.
|
|
//#define HOST_CHECKS
|
|
|
|
// just the plain ip number of the remote host
|
|
// ^^ glatte lüge!
|
|
volatile string peerhost;
|
|
#if 0 //defined(PSYC_TCP) && __EFUN_DEFINED__(strrstr)
|
|
volatile string peerdomain;
|
|
#endif
|
|
// remote port number
|
|
volatile int peerport;
|
|
// unresolved-ip or ip:port
|
|
volatile string peeraddr;
|
|
// holds last ip we were connected to
|
|
volatile string peerip;
|
|
#ifdef SYSTEM_SECRET
|
|
// how much can we trust the content of this packet?
|
|
volatile int ctrust;
|
|
volatile string checkpack;
|
|
volatile mixed vcheck;
|
|
#endif
|
|
|
|
// current variables (":"), permanent variables ("=", "+", "-")
|
|
//
|
|
volatile mapping cvars = 0, pvars = ([ "_INTERNAL_origin" : ME ]);
|
|
//
|
|
// a distinction between mmp and psyc-vars should be made
|
|
|
|
// current method and incoming buffer
|
|
volatile string mc, buffer;
|
|
|
|
#ifdef REPATCHING
|
|
// cache of patched remote uniforms
|
|
volatile mapping patches = ([]);
|
|
#endif
|
|
|
|
// parsing helpers..
|
|
volatile string lastvar, mod, origin_unl;
|
|
//
|
|
// list parsing helpers..
|
|
volatile array(mixed) list;
|
|
volatile mapping hash;
|
|
volatile int l = 0;
|
|
volatile int pongtime, routing = 1;
|
|
|
|
#ifndef PSYC_TCP
|
|
// resolved UNL of remote server (psyc://hostname or psyc://hostname:port)
|
|
volatile string netloc;
|
|
# define QUIT return 1; // udp only definition
|
|
#endif
|
|
|
|
// REPATCHING! (temporarily called ifdef SCHWAN)
|
|
// we have two alternate ways to deal with unknown incoming connections
|
|
// the default one is to double check the hostname and return an error
|
|
// if the name isn't valid. the second one is to repatch unknown hosts
|
|
// into their ip numbers. the latter we obviously normally don't use
|
|
#ifndef REPATCHING
|
|
// prototype definition for #'getdata
|
|
protected int deliver(mixed ip, string host, string mc, string buffer, mapping cvars);
|
|
#endif
|
|
|
|
vamixed getdata(string a);
|
|
|
|
#ifdef __LDMUD__
|
|
// i think once upon a time the '&& vname' at the end wasn't necessary..
|
|
# define SCANFIT sscanf(a, "%1.1s%s%t%s", mod, vname, vvalue) && vname
|
|
#else
|
|
# define SCANFIT sscanf(a, "%1s%s%*[ \t]%s", mod, vname, vvalue) && vname
|
|
#endif
|
|
|
|
#define SPLITVAL(val) \
|
|
unless (sscanf(val, "%s %s", val, vdata)) vdata = 0
|
|
// sollte es wirklich ein space sein? hmmm.. %t ist sowieso zu grob..
|
|
|
|
// i would like to revert to a parser that does not need to merge
|
|
// mappings at the end of each packet, just because someone
|
|
// may think a '=' does not affect a ':'. the mmp spec will
|
|
// clarify. it's not a reason to mess up a parser for!
|
|
// the simplicity of the parsers is an objective worth
|
|
// making some things clearer in the spec rather than here!
|
|
#ifdef DONT_MERGE
|
|
# if __EFUN_DEFINED__(copy)
|
|
# define INITCVARS cvars = copy(pvars);
|
|
# else
|
|
# define INITCVARS cvars = ([ ]) + pvars;
|
|
# endif
|
|
#else
|
|
// iff we stick to this ugly variant, we can at least simplify some operations
|
|
// which are being done on both pvars and cvars.. TODO
|
|
# define MERGEVARS cvars = pvars + cvars;
|
|
#endif
|
|
// el explains:
|
|
// because in case we do a cvars = copy(pvars) in
|
|
// restart() operations on _state go wrong. imagine
|
|
// someone sends this packet:
|
|
//
|
|
// :_source psyc://someone
|
|
// =_source
|
|
//
|
|
// this would result in _source beeing empty, which is
|
|
// wrong. or do we enforce everyone to send permanent
|
|
// changes before a _set? or do we even prohibit sending
|
|
// the same var with different modifiers ?
|
|
// <lynX> yes, permanent changes need to come before the :
|
|
// the other way round is just begging to be misunderstood
|
|
|
|
// state inspector for 20after4
|
|
mixed qState(string vname) { return pvars[vname]; }
|
|
|
|
int restart() {
|
|
// delete temporary variables for next msg
|
|
cvars = ([ ]);
|
|
// delete other stuff too
|
|
buffer = "";
|
|
mc = 0;
|
|
routing = 1; // unused as yet
|
|
#ifdef SYSTEM_SECRET
|
|
checkpack = vcheck = 0;
|
|
ctrust = trustworthy;
|
|
#endif
|
|
return 1;
|
|
}
|
|
|
|
private int listAppend(string vname, string vvalue) {
|
|
string vdata;
|
|
|
|
unless (vname) {
|
|
croak("_error_syntax_list_append",
|
|
"Invalid list append without preceding variable assignment.");
|
|
QUIT
|
|
}
|
|
unless (list) {
|
|
// eek. such an array has a size of ~35 kb right after
|
|
// allocation. we should not do that!
|
|
list = allocate(PSYC_LIST_SIZE_LIMIT);
|
|
ASSERT("listAppend", stringp(cvars[vname]), buffer)
|
|
SPLITVAL(cvars[vname]);
|
|
list[l = 0] = cvars[vname];
|
|
if (vdata) hash = ([ cvars[vname] : vdata ]);
|
|
}
|
|
SPLITVAL(vvalue);
|
|
if (hash) hash[vvalue] = vdata;
|
|
else if (vdata) {
|
|
// should this ever happen?
|
|
hash = mkmapping(list, allocate(sizeof(list)));
|
|
hash[vvalue] = vdata;
|
|
}
|
|
list[++l] = vvalue;
|
|
return 1;
|
|
}
|
|
|
|
private int conclude() {
|
|
P3(("pp::conclude lastvar:%O l:%O hash:%O\n", lastvar, l, hash))
|
|
unless (lastvar) return 0;
|
|
// if (abbrev("_INTERNAL", lastvar)) not necessary because
|
|
// the following check only allows lowercase for now
|
|
if (
|
|
#ifdef EXPERIMENTAL
|
|
!legal_keyword(lastvar)
|
|
#else
|
|
// very relaxed strategy
|
|
strlen(lastvar) < 2 || lastvar[0] != '_' // policy may change
|
|
|| lastvar[1] > 'z' || lastvar[1] < 'a'
|
|
#endif
|
|
) {
|
|
croak("_error_illegal_protocol_variable",
|
|
"You are not allowed to present a variable named [_variable_name].",
|
|
([ "_variable_name": lastvar ]));
|
|
QUIT
|
|
}
|
|
if (l) {
|
|
// alright, we have a list to close.. was: listEnd()
|
|
if (mappingp(hash)) {
|
|
cvars[lastvar] = hash;
|
|
hash = 0;
|
|
} else
|
|
cvars[lastvar] = list[0..l];
|
|
list = 0;
|
|
l = 0;
|
|
P2(("%O concluded %O (%O)\n", ME, lastvar, cvars[lastvar]))
|
|
//
|
|
// at this point we would need to know if the mod was =
|
|
// to also store the list in pvars, but since i'm generally
|
|
// doubtful if this is the right way to go, i'll leave it to
|
|
// a later day.. TODO
|
|
// hooray, we get to check some variable families for sanity
|
|
} else if (abbrev("_degree", lastvar)) {
|
|
// allow for unset degree '-'
|
|
if (!stringp(cvars[lastvar]) || (cvars[lastvar] != "-" &&
|
|
!sscanf(cvars[lastvar], "%1d", cvars[lastvar]))) {
|
|
P1(("%O failed to parse %O: %O\n", ME,
|
|
lastvar, cvars[lastvar]))
|
|
m_delete(cvars, lastvar);
|
|
if (mod != ":") m_delete(pvars, lastvar);
|
|
}
|
|
else if (mod != ":") pvars[lastvar] = cvars[lastvar];
|
|
} else if (abbrev("_list", lastvar)) { // _tab
|
|
// we only get here if the _list has one or zero members
|
|
#ifdef PARANOID
|
|
unless (stringp(cvars[lastvar])) {
|
|
PT(("%O wrrrooonnggg attempt to fix %O (%O)\n", ME,
|
|
lastvar, cvars[lastvar]))
|
|
} else
|
|
#endif
|
|
if (strlen(cvars[lastvar]))
|
|
cvars[lastvar] = ({ cvars[lastvar] });
|
|
else
|
|
cvars[lastvar] = ({ });
|
|
P2(("%O incoming %O (%O)\n", ME, lastvar, cvars[lastvar]))
|
|
// an empty variable is an empty variable, not zero
|
|
// } else unless (strlen(cvars[lastvar])) {
|
|
// cvars[lastvar] = 0;
|
|
}
|
|
P3(("pp::conclude cvars:%O\n", cvars))
|
|
lastvar = 0;
|
|
return 0;
|
|
}
|
|
|
|
varargs void diminish(string vname, vastring vvalue) {
|
|
string vdata; // dummy
|
|
|
|
// D4(D("diminish("+vname+","+vvalue+")\n");)
|
|
if (!vname || vname=="") vname = lastvar;
|
|
else {
|
|
conclude();
|
|
lastvar = vname;
|
|
}
|
|
if (!member(pvars, vname)) {
|
|
D4( D("diminish: no such variable '"+vname+"'\n"); )
|
|
return 0;
|
|
}
|
|
if (vvalue && vvalue != "") {
|
|
SPLITVAL(vvalue);
|
|
if (pvars[vname] == vvalue) {
|
|
m_delete(pvars, vname);
|
|
m_delete(cvars, vname);
|
|
} else if (mappingp(pvars[vname])) {
|
|
m_delete(pvars[vname], vvalue);
|
|
// should be same mapping anyway
|
|
m_delete(cvars[vname], vvalue);
|
|
} else {
|
|
log_file("PSYC", "%O DIMINISH %O from %O\n",
|
|
query_ip_name(), vvalue, pvars[vname]);
|
|
// this is critical enough to let our counterpart know
|
|
croak("_failure_unsupported_modifier_diminish",
|
|
"Sorry, can't diminish that.");
|
|
}
|
|
} else {
|
|
if (member(pvars, vname)) {
|
|
m_delete(pvars, vname);
|
|
m_delete(cvars, vname);
|
|
} else {
|
|
// javapsyc currently sends diminish twice, bug..
|
|
D3( D("diminish: no such variable '"+vname+"'\n"); )
|
|
// D3 means we don't want to know about it ;-)
|
|
}
|
|
}
|
|
}
|
|
|
|
vamixed parse(string a) {
|
|
int i;
|
|
string vname, vvalue, vdata;
|
|
#ifdef _flag_log_sockets_PSYC
|
|
log_file("RAW_PSYC", "%O «\t%s\n", ME, a);
|
|
#endif
|
|
// P3(("CVARS (%O):\n %O, %O\n", a, query_ip_number(ME), ME))
|
|
D3( if (a && strlen(a) > 2) D(S("pp:parse %O\n", a)); )
|
|
#ifdef SYSTEM_SECRET
|
|
if (checkpack) checkpack += a + "\n";
|
|
#endif
|
|
// if (a == ".") {
|
|
// PT((">>>>> RESTARTING because of .. strange '.\\n'\n"))
|
|
// restart();
|
|
// }
|
|
// else if (a[<1] == '\\') {
|
|
// buffer += a[..<2];
|
|
// D1( D("psyc-parse-multiline: "+buffer+"\n"); )
|
|
// }
|
|
// else
|
|
if (!a || a == "") {
|
|
// at this point we move on from mmp_parse to psyc_parse.
|
|
// <lynX> with bkstate coming our way we will soon have
|
|
// to put the mmp variable state away somewhere
|
|
// and go fetch the psyc variable state for the current
|
|
// source/target pair from somewhere else. TODO
|
|
routing = 0; // unused as yet
|
|
} else switch(a[0]) {
|
|
case ':':
|
|
unless (SCANFIT) {
|
|
// compatible to new syntax
|
|
if (vname && strlen(vname)) {
|
|
m_delete(cvars, vname);
|
|
lastvar = 0;
|
|
} else {
|
|
P1(("PSYC got %O from %O\n", a, query_ip_number()))
|
|
croak("_error_syntax_set",
|
|
"Couldn't parse that variable setting.");
|
|
QUIT
|
|
}
|
|
}
|
|
if (vname != "") {
|
|
conclude();
|
|
cvars[lastvar = vname] = vvalue;
|
|
#ifdef SYSTEM_SECRET
|
|
unless (vcheck) {
|
|
// ignore checksum if origin is trustworthy
|
|
// already.. function only available in ':'
|
|
if (trustworthy<8 && abbrev("_check", vname)) {
|
|
vcheck = vname;
|
|
checkpack = "";
|
|
// pvars invalid in checked packets
|
|
// huh? das ist doch quatsch!
|
|
//cvars = ([]);
|
|
// _INTERNAL_origin will be undefined!
|
|
} else vcheck = -1;
|
|
// only first variable setting may set checksum
|
|
}
|
|
#endif
|
|
} else listAppend(lastvar, vvalue);
|
|
break;
|
|
case '=':
|
|
// currently psyced cannot handle "=_target" (to clear a var)
|
|
// it requires a whitespace after that (like a \t char).. TODO
|
|
unless (SCANFIT) {
|
|
croak("_error_syntax_assign",
|
|
"Couldn't parse that variable assignment.");
|
|
QUIT
|
|
}
|
|
if (vname != "") { // // ensure varname is legal?
|
|
conclude();
|
|
lastvar = vname;
|
|
pvars[vname] = vvalue;
|
|
#ifdef DONT_MERGE
|
|
cvars[vname] = vvalue;
|
|
#endif
|
|
} else listAppend(lastvar, vvalue);
|
|
break;
|
|
case '+':
|
|
unless (SCANFIT) {
|
|
croak("_error_syntax_augment",
|
|
"Couldn't parse that variable addition.");
|
|
QUIT
|
|
}
|
|
if (vname != "") {
|
|
conclude();
|
|
lastvar = vname;
|
|
} else vname = lastvar;
|
|
|
|
unless (mappingp(pvars[vname])) {
|
|
SPLITVAL(pvars[vname]);
|
|
pvars[vname] = ([ pvars[vname] : vdata ]);
|
|
}
|
|
SPLITVAL(vvalue);
|
|
pvars[vname][vvalue] = vdata;
|
|
#ifdef DONT_MERGE
|
|
cvars[vname] = pvars[vname];
|
|
#endif
|
|
break;
|
|
case '-':
|
|
if (SCANFIT)
|
|
diminish(vname, vvalue);
|
|
else
|
|
diminish(a[1..]);
|
|
break;
|
|
case '?':
|
|
// should generate a query message of it own?
|
|
D2( D(a+" from PSYC/TCP ignored..\n"); )
|
|
log_file("PSYC", "%O QUERY %O\n", query_ip_name(), a);
|
|
break;
|
|
case '\t':
|
|
case ' ': // var continuation
|
|
// tab is preferred, but space is also allowed
|
|
vvalue = a[1..];
|
|
P4(("continuation.. %s %O %O\n", mod, lastvar, vvalue))
|
|
if (mod == "-") {
|
|
// croak("_error_syntax_diminish_continued",
|
|
// "Line continuation not defined for diminish.");
|
|
// QUIT
|
|
log_file("PSYC",
|
|
"%O SEMANTIX _error_syntax_diminish_continued\n", query_ip_name());
|
|
// since diminish of lists only uses first word
|
|
// as list key, anything that follows must be junk
|
|
// so we throw it away
|
|
break;
|
|
}
|
|
#ifdef DONT_MERGE
|
|
cvars[lastvar] += "\n" + vvalue;
|
|
if (mod != ":") pvars[lastvar] = cvars[lastvar];
|
|
#else
|
|
if (mod == ":") cvars[lastvar] += "\n" + vvalue;
|
|
else pvars[lastvar] += "\n" + vvalue;
|
|
#endif
|
|
break;
|
|
case '.':
|
|
// empty packet, at least without mc..
|
|
// since we dont distinguish between mmp and psyc vars here..
|
|
conclude();
|
|
if (sizeof(cvars) == 0) {
|
|
#ifndef __PIKE__
|
|
if (peerip && pongtime + 120 < time()) {
|
|
if (same_host(SERVER_HOST, peerip)) {
|
|
P1(("why am i talking psyc to myself?\n"))
|
|
} else {
|
|
#ifdef PSYC_TCP
|
|
P2(("%O sending TCP PONG to %O=%O\n",
|
|
ME, peerip, peerhost))
|
|
emit(".\n");
|
|
#else
|
|
P1(("%O sending UDP PONG to %O=%O\n",
|
|
ME, peerip, peerhost))
|
|
send_udp(peerip, peerport, ".\n");
|
|
#endif
|
|
}
|
|
pongtime = time();
|
|
}
|
|
#endif
|
|
} else if (!member(cvars, "_source")) {
|
|
// this is a change of state.
|
|
MERGEVARS // until we have proper array support..
|
|
rootMsg(0, 0, 0, cvars);
|
|
// why do we deliver an empty message anyway? shouldn't
|
|
// we deliver the + - and = events instead? i mean..
|
|
// figuring out which vars have changed is so much hairier!?
|
|
}
|
|
restart();
|
|
break;
|
|
case '_':
|
|
// default:
|
|
conclude();
|
|
if (cvars["_length"]) {
|
|
croak("_failure_unsupported_module_length",
|
|
"Sorry, cannot deal with _length yet.");
|
|
QUIT
|
|
}
|
|
MERGEVARS
|
|
#ifdef BITKOENIG_SYNTAX
|
|
sscanf(a, "%s %s", a, buffer);
|
|
#endif
|
|
#ifdef EXPERIMENTAL
|
|
if (!legal_keyword(a))
|
|
#else
|
|
// pretty inefficient strategy here
|
|
for (i=strlen(a)-1; i>=0; i--)
|
|
unless (a[i] == '_' ||
|
|
(a[i] >= 'a' && a[i] <= 'z') ||
|
|
(a[i] >= '0' && a[i] <= '9') ||
|
|
(a[i] >= 'A' && a[i] <= 'Z'))
|
|
#endif
|
|
{
|
|
croak("_error_illegal_method",
|
|
"That's not a valid method name.");
|
|
QUIT
|
|
}
|
|
mc = a;
|
|
#ifdef PSYC_TCP
|
|
if (abbrev("_data", mc)) {
|
|
croak("_failure_unsupported_method_data",
|
|
"Sorry, cannot deal with binary data yet.");
|
|
QUIT
|
|
}
|
|
next_input_to(#'getdata);
|
|
#endif
|
|
conclude();
|
|
P3(("»pp:method(m:%O, v:%O)\n", mc, cvars))
|
|
switch(mc) {
|
|
// okay.. this change needs more preparation.. psyced aint ready for this :(
|
|
// sorry ppl -lnyX
|
|
// case "_message_public":
|
|
// case "_message_private":
|
|
case "_conversation": // we will soon have to decide its final name...
|
|
case "_converse":
|
|
case "_talk":
|
|
mc = "_message";
|
|
break;
|
|
case "_notice_circuit_established":
|
|
unless (origin_unl) origin_unl = cvars["_source"];
|
|
break;
|
|
case "_request_circuit_shutdown":
|
|
#ifdef PSYC_TCP
|
|
PT(("%O got %s <- selfdestructing.\n", ME, mc))
|
|
destruct(ME);
|
|
#else
|
|
P0(("%O got %s, but we HAVE no circuit!\n", ME, mc))
|
|
#endif
|
|
return 1;
|
|
}
|
|
return 1;
|
|
default:
|
|
log_file("PSYC", "%O SYNTAX %O\n", query_ip_name(), a);
|
|
croak("_error_invalid_method_compact",
|
|
"Compact methods undefined as yet.");
|
|
QUIT
|
|
}
|
|
#ifdef PSYC_TCP
|
|
next_input_to(#'parse);
|
|
if (flags & TCP_PENDING_TIMEOUT) {
|
|
remove_call_out(#'quit);
|
|
flags -= TCP_PENDING_TIMEOUT;
|
|
}
|
|
#endif
|
|
#ifndef __PIKE__
|
|
return 1;
|
|
#endif
|
|
}
|
|
|
|
#ifdef REPATCHING // {{{
|
|
private repatch(string t) {
|
|
//if (patches[t]) {
|
|
if (0) {
|
|
D3(D("psyc:parse * repatched "+t+" to "+patches[t]+"\n");)
|
|
} else {
|
|
array(mixed) u = parse_uniform(t);
|
|
|
|
#if 0 //defined(PSYC_TCP) && __EFUN_DEFINED__(strrstr)
|
|
unless (peerdomain) {
|
|
// if (abbrev("ve.", peerhost)) {
|
|
int x = strrstr(peerhost, ".");
|
|
if (x>=0) x = strrstr(peerhost, ".", -x);
|
|
if (x>=0) peerdomain = peerhost[x..];
|
|
// }
|
|
else peerdomain = "X";
|
|
P3(("peerdomain %O of %O\n",
|
|
peerdomain, peerhost))
|
|
}
|
|
#endif
|
|
// wir vertrauen der port nummer!
|
|
P3(("···· UHost=%O peerhost=%O URoot=%O\n",
|
|
u[UHost], peerhost, u[URoot]))
|
|
if (u[UHost] != peerhost
|
|
&& !same_host(u[UHost], peerhost)
|
|
#if 0 //defined(PSYC_TCP) && __EFUN_DEFINED__(strrstr)
|
|
&& !trail(peerdomain, u[UHost])
|
|
#endif
|
|
&& find_target_handler(u[URoot]) != ME) {
|
|
u[UHost] = peerhost;
|
|
patches[t] = render_uniform(u);
|
|
D2(D("psyc:parse * patched "+t+" to "+patches[t]+"\n");)
|
|
} else {
|
|
P3(("psyc:parse * "+t+" not patched\n"))
|
|
patches[t] = t;
|
|
}
|
|
|
|
register_target(patches[t]);
|
|
}
|
|
P3(("repatch: %O -> %O = %O\n", ME, peerhost, patches[t]))
|
|
return patches[t];
|
|
}
|
|
#endif // }}}
|
|
|
|
vamixed getdata(string a) {
|
|
#ifdef _flag_log_sockets_PSYC
|
|
log_file("RAW_PSYC", "%O «\t%s\n", ME, a);
|
|
#endif
|
|
if (a != ".") {
|
|
#ifdef SYSTEM_SECRET
|
|
if (checkpack) checkpack += a + "\n";
|
|
#endif
|
|
if (buffer != "") buffer += "\n";
|
|
buffer += a;
|
|
#ifdef PSYC_TCP
|
|
next_input_to(#'getdata);
|
|
#endif
|
|
} else {
|
|
#ifndef REPATCHING
|
|
array(mixed) u;
|
|
string t = cvars["_context"] || cvars["_source"];
|
|
|
|
# ifdef PSYC_TCP
|
|
// let's do this before we deliver in case we run into
|
|
// a runtime error (but it's still better to fix it!)
|
|
next_input_to(#'parse);
|
|
# endif
|
|
if (!t || trustworthy > 5) {
|
|
deliver(0, 0, mc, buffer, cvars);
|
|
} else unless (u = parse_uniform(t)) {
|
|
croak("_error_invalid_uniform",
|
|
"Looks like a malformed URL to me.");
|
|
QUIT
|
|
// henner suggests this shouldn't be a QUIT.. TODO
|
|
#ifdef __PIKE__
|
|
}
|
|
deliver(0, 0, mc, buffer, cvars);
|
|
#else
|
|
# ifdef PSYC_TCP
|
|
// Authenticated
|
|
} else if (qAuthenticated(NAMEPREP(u[UHost]))) {
|
|
if (u[UTransport] && (u[UTransport] !=
|
|
tls_query_connection_state() ? "s" : "c")) {
|
|
P1(("%O sends me wrong transport %O in %O\n",
|
|
ME, u[UTransport], t))
|
|
croak("_error_invalid_uniform_transport",
|
|
"[_uniform] promotes wrong transport '[_transport]'.",
|
|
([ "_uniform": t,
|
|
"_transport": u[UTransport] ]));
|
|
// QUIT
|
|
} else {
|
|
P2(("%O has accepted %O by qAuthenticated auth\n", ME, t))
|
|
deliver(0, 0, mc, buffer, cvars);
|
|
}
|
|
} else if (tls_query_connection_state()) {
|
|
# ifdef _flag_report_bogus_certificates
|
|
monitor_report("_warning_invalid_host_certificate",
|
|
S("%O presented an uncertified host %O.",
|
|
ME, u[UHost]));
|
|
# else
|
|
P1(("%O presented an uncertified host %O.",
|
|
ME, u[UHost]))
|
|
# endif
|
|
# ifdef _flag_log_bogus_certificates
|
|
log_file("CERTS", S("%O %O %O\n",
|
|
ME, u[UHost], tls_certificate(ME, 0)));
|
|
# endif
|
|
// vs. _flag_reject_bogus_certificates huh?
|
|
# ifndef _flag_allow_invalid_host_certificate
|
|
croak("_error_invalid_host_certificate",
|
|
"[_host] is not an authenticated source host.",
|
|
([ "_host": u[UHost] ]));
|
|
QUIT
|
|
# endif
|
|
# endif
|
|
// fippo suggests we should have a protocol that enables
|
|
// hostnames on the other side first, before we deliver data
|
|
// to it. currently this could be abused for a DoS attack.
|
|
//
|
|
// in the case of udp without udp circuits, such an exchange
|
|
// doesn't make sense: udp psyc is intended to be lightweight.
|
|
// in that case we shouldn't resolve here, we should simply
|
|
// reject the source information, or repatch it. the fact that
|
|
// we are receiving messages is still useful. better even if
|
|
// we had a cryptographic way to protect udp packets, making
|
|
// even potential ip spoofing irrelevant. -lynX
|
|
//
|
|
} else dns_resolve(u[UHost], #'deliver,
|
|
u[UHost], mc, buffer, cvars);
|
|
#endif
|
|
restart();
|
|
}
|
|
return 1;
|
|
}
|
|
// we are in ifndef REPATCHING
|
|
// this additional function to handle name resolution does not
|
|
// exist in REPATCHING mode. see explanation at the top of the file.
|
|
protected int deliver(mixed ip, string host, string mc, string buffer, mapping cvars) {
|
|
string psycaddr;
|
|
|
|
# if 0 //def __PIKE__
|
|
// temporary!
|
|
return doneParse(ip, host, mc, buffer, cvars);
|
|
# else
|
|
// PT(("deliver(%O, %O, %O, %O, %O)\n", ip, host, mc, buffer, cvars))
|
|
// when erq isn't running we get ip == host .. maybe return -1 instead?
|
|
// and why return -1 for failed dns when we could use 0 ? FIXME
|
|
// no wait.. why is ip==0 being let thru in this code? did i forget
|
|
// about something?
|
|
//# ifdef PSYC_TCP
|
|
if (ip) {
|
|
//# endif
|
|
if (ip == -1) {
|
|
monitor_report("_warning_invalid_host",
|
|
S("%O could not resolve %O.", ME, host));
|
|
croak("_error_invalid_host", "Could not resolve [_host].",
|
|
([ "_host": host ]));
|
|
QUIT
|
|
return 1;
|
|
} else if (ip == host) {
|
|
// Happens when 'erq' could not be spawned. We allow
|
|
// the message through anyway (thus spoofable) and
|
|
// just complain about it.
|
|
monitor_report("_warning_unavailable_resolution",
|
|
S("%O could not resolve %O. Good luck!", ME, host));
|
|
// This is a clear case of a _failure, but if you insist
|
|
// on running your psyced without dns resolution, who are
|
|
// we to intentionally break your experience of unsafety?
|
|
} else if (ip == peerip) {
|
|
//PT(("deliver: from %O\n", peerhost))
|
|
# ifdef STRICT_DNS_POLICY
|
|
if (peerhost == peerip) {
|
|
P0(("deliver: got %O from %O but host %O is not resolved yet.\n", mc, ip, peerhost))
|
|
croak("_error_invalid_host_slow",
|
|
"Could not resolve [_host] quickly enough for you!",
|
|
([ "_host": peerip ]));
|
|
QUIT
|
|
//return 1;
|
|
}
|
|
# endif
|
|
//# ifdef PSYC_TCP
|
|
} else {
|
|
monitor_report("_warning_rejected_relay_incoming",
|
|
"Dropped a packet from "+ peerip +
|
|
" trying to relay for "+ host +" ("+ ip +")");
|
|
P3(("Dropped a packet from "+ peerip +
|
|
" trying to relay for "+ host +" ("+ ip +")"+"\n"+
|
|
" in parse:deliver(%O, %O, %O, %O, %O)\n",
|
|
ip, host, mc, buffer, cvars))
|
|
croak("_error_rejected_relay_incoming",
|
|
"Your address [_host_IP] is not permitted to send as [_host].",
|
|
([ "_host": host, "_host_IP": peerip ]));
|
|
QUIT
|
|
return 1;
|
|
}
|
|
//# endif
|
|
}
|
|
# ifdef PSYC_TCP
|
|
else if (psycaddr = cvars["_context"] || cvars["_source"]) {
|
|
register_target(lower_case(psycaddr));
|
|
}
|
|
# endif
|
|
# endif // PIKE
|
|
{ // looks strange but is correct
|
|
#endif // !REPATCHING
|
|
mixed source, context, t, t2;
|
|
object o;
|
|
int i;
|
|
|
|
P3(("parsed: (%O,%O,%O from %O)\n", mc, buffer, cvars, pvars))
|
|
|
|
#ifndef __PIKE__
|
|
#ifdef PSYC_TCP
|
|
if (host) sAuthenticated(host); // dns resolution just once
|
|
#endif
|
|
#ifdef SYSTEM_SECRET
|
|
if (checkpack) {
|
|
// TODO: extend logic to allow for per-host secrets
|
|
// for example using the /config command..
|
|
switch (vcheck) {
|
|
#if __EFUN_DEFINED__(hmac)
|
|
# ifndef DONT_TRUST_SHA224
|
|
case "_check_SHA224":
|
|
break;
|
|
# endif
|
|
# ifndef DONT_TRUST_SHA256
|
|
case "_check_SHA256":
|
|
ctrust = (hmac(TLS_HASH_SHA256,
|
|
// TODO: hash(TLS_HASH_SHA256, SYSTEM_SECRET) only needs to be computed once
|
|
// for the entire system, and stored in the library.
|
|
hash(TLS_HASH_SHA256, SYSTEM_SECRET),
|
|
checkpack) == lower_case(cvars[vcheck])) ? 8: 0;
|
|
break;
|
|
# endif
|
|
# ifndef DONT_TRUST_SHA512
|
|
case "_check_SHA512":
|
|
break;
|
|
# endif
|
|
#endif
|
|
// should move out of #ifdef SYSTEM_SECRET if
|
|
// anyone actually likes to use this
|
|
case "_check_integrity_SHA1":
|
|
if (sha1(checkpack) !=
|
|
lower_case(cvars[vcheck])) {
|
|
croak("_error_invalid_integrity",
|
|
"Strange, integrity check [_integrity] failed.",
|
|
([ "_integrity": cvars[vcheck] ]) );
|
|
return 1;
|
|
}
|
|
break;
|
|
#ifndef DONT_TRUST_SHA1
|
|
case "_check_SHA1":
|
|
ctrust = (sha1(SYSTEM_SECRET + checkpack) ==
|
|
lower_case(cvars[vcheck])) ? 8 : 0;
|
|
break;
|
|
#endif
|
|
case "_check_fake":
|
|
ctrust = (md5(SYSTEM_SECRET) ==
|
|
lower_case(cvars[vcheck])) ? 8 : 0;
|
|
break;
|
|
case "_check":
|
|
#ifndef DONT_TRUST_MD5
|
|
case "_check_MD5":
|
|
ctrust = (md5(SYSTEM_SECRET + checkpack) ==
|
|
lower_case(cvars[vcheck])) ? 8 : 0;
|
|
//PT(("checksum %O == %O for %O + %O results in %O\n", md5(SYSTEM_SECRET + checkpack), cvars, SYSTEM_SECRET, checkpack, ctrust))
|
|
break;
|
|
#endif
|
|
default:
|
|
// _warn_unsupported_check_method ?
|
|
// ctrust remains at host-ip level
|
|
break;
|
|
}
|
|
//PT(("psyc:checksum result %O with packet contents %O\n", ctrust, checkpack))
|
|
// a quick solution.. maybe we should do SASL instead, or maybe
|
|
// we should let the other side specify its =_trust whenever the
|
|
// packet is auth'd by a checkpack
|
|
if (mc == "_request_circuit_trust") {
|
|
// oops.. major error in logic here.. i should be sending a challenge first!!
|
|
if (ctrust > 6) {
|
|
trustworthy = ctrust;
|
|
croak("_echo_circuit_trust",
|
|
"You have gained my trust.");
|
|
return 1;
|
|
}
|
|
croak("_failure_invalid_circuit_trust",
|
|
"Sorry, could not verify your authorization.");
|
|
return 1;
|
|
}
|
|
}
|
|
if (ctrust) cvars["_INTERNAL_trust"] = ctrust;
|
|
#else
|
|
// good placement here?
|
|
if (mc == "_request_circuit_trust") {
|
|
croak("_failure_invalid_circuit_trust",
|
|
"Sorry, could not verify your authorization.");
|
|
return 1;
|
|
}
|
|
#endif
|
|
#endif // PIKE
|
|
#if 1 // def DONT_MERGE
|
|
// das klappert irgendwie nicht als pvar TODO
|
|
cvars["_INTERNAL_origin"] = ME;
|
|
// wieso nicht? warum nicht lieber den bug suchen statt
|
|
// hier was rumzuhacken? vermutlich kann man es nicht
|
|
// oben zuweisen, muss also in ein create()
|
|
#endif
|
|
#ifdef TRANSMITTING_FAKE_TIME_IS_A_CAPITAL_CRIME
|
|
// first of all we can not allow _time, since that is
|
|
// used internally for last-log functionality and would
|
|
// irritate the showLog() mechanism in user.c
|
|
//
|
|
m_delete(cvars, "_time");
|
|
#endif
|
|
//
|
|
// this may only be set by user:msg()
|
|
m_delete(cvars, "_nick_verbatim");
|
|
|
|
#ifdef PSYC_TCP
|
|
# ifdef REPATCHING
|
|
next_input_to(#'parse);
|
|
# endif
|
|
# if 0 //ndef HOST_CHECKS
|
|
unless (netloc) {
|
|
// lets see if the ip number has a name
|
|
if (query_ip_name() != query_ip_number()) {
|
|
netloc = "psyc://"+query_ip_name();
|
|
// peerport has either positive or negative value
|
|
if (peerport) netloc += ":"+peerport;
|
|
D2(D(netloc+ " resolved name registered\n");)
|
|
register_target( netloc );
|
|
}
|
|
}
|
|
# endif
|
|
#endif
|
|
// backward? compatibility to jaPSYC library
|
|
// if (abbrev("_diminish", mc)) {
|
|
// diminish(mc[9..], buffer);
|
|
// return restart();
|
|
// }
|
|
if (t = cvars["_context"]) {
|
|
// detect when _context contains a local object
|
|
// which of course should never happen according
|
|
// to routing rules. should we check for psyc_object()?
|
|
// no, we should check the validity of the hostnames
|
|
// sent to us, then it couldn't possibly be a local object.
|
|
if (objectp(t)) {
|
|
// and no, it's not in pvars
|
|
PP(("psyc:parse got object!? %O.", t));
|
|
context = t;
|
|
// lowercase policy
|
|
} else context = cvars["_context"] = lower_case(t);
|
|
}
|
|
if (t = cvars["_source"]) {
|
|
// "Link to [_source] established.. uses this
|
|
// and place/slave too (although improperly)
|
|
//m_delete(cvars, "_source");
|
|
|
|
// if a context is talking, we recognize local sources
|
|
// this may not be up to date with new routing syntax :(
|
|
unless (context && (source = psyc_object(t))) {
|
|
#if 0 // currently we don't use any relative object specs
|
|
i = strstr(t, "//");
|
|
if (i < 0) { // a relative object spec! experimental!!
|
|
// lets guess the remote UNL
|
|
source = "psyc://"+peeraddr;
|
|
// we allow with or without leading slash
|
|
if (t[0] != '/') source += "/";
|
|
// we used to care about a trailing slash, why?
|
|
//if (t[strlen(t)-1] != '/') t += "/";
|
|
// maybe we should strip trailing slashes instead
|
|
source += t;
|
|
} else if (i == 0) { // a shortened object spec
|
|
source = "psyc:"+t;
|
|
} else source = t;
|
|
#endif
|
|
// lowercase policy
|
|
source = lower_case(t);
|
|
}
|
|
#ifndef NOT_EXPERIMENTAL
|
|
} else {
|
|
source = "psyc://"+peeraddr+"/";
|
|
# ifdef REPATCHING
|
|
patches[source] = source; // dont parse this
|
|
# endif
|
|
#endif
|
|
}
|
|
// can this safely move into one of the ifs above? TODO
|
|
if (t = cvars["_source_relay"]) {
|
|
// recognize local psyc object
|
|
if (t = psyc_object(t)) {
|
|
P2(("local _source_relay %O for %O from %O, cont %O\n",
|
|
t, mc, source, context))
|
|
cvars["_source_relay"] = t;
|
|
} else if (stringp(t)) {
|
|
// otherwise: lowercase policy
|
|
cvars["_source_relay"] = lower_case(t);
|
|
}
|
|
}
|
|
P3(("parse1: source is %O, context is %O\n", source, context))
|
|
// other side doesn't *have* to provide a source..
|
|
if (trustworthy > 6 && (t2 = context || source))
|
|
// according to http://about.psyc.eu/Routing we should
|
|
// not receive a message with both context and source in
|
|
// a long time, so the order of OR is irrelevant here.
|
|
register_target(t2);
|
|
// cache the physical source string so we don't need to
|
|
// parse it later
|
|
#ifdef REPATCHING // {{{
|
|
else {
|
|
// virtual hosting support müsste evtl hierhin.. TODO
|
|
if (context) {
|
|
// now it becomes the job of the person.c to figure
|
|
// out if the context is legitimate and trustworthy
|
|
// psyc1.1 type multicasting not supported here
|
|
context = cvars["_context"] = repatch(context);
|
|
} else if (source) {
|
|
D1( if (objectp(source)) D("something went wrong.\n"); )
|
|
cvars["_source_verbatim"] = source;
|
|
source = repatch(source);
|
|
}
|
|
}
|
|
P3(("parse2: source is %O, context is %O\n", source, context))
|
|
#endif // REPATCHING }}}
|
|
if (t = cvars["_target"]) {
|
|
// this isn't used anywhere, apparently
|
|
// but the recipient may care to know how he got called
|
|
//m_delete(cvars, "_target");
|
|
o = psyc_object(t); // find the object matching the _target
|
|
if (!o && strlen(t)) {
|
|
mixed u = parse_uniform(t, 1);
|
|
// be tolerant, allow for scheme-independent notation
|
|
// //unless (u[UScheme] && u[UScheme] == "psyc") {
|
|
// unless (u[UScheme] && strlen(u[UScheme])) {
|
|
// croak("_failure_unsupported_target_relative",
|
|
// "No support for relative targets yet.");
|
|
// QUIT
|
|
// }
|
|
unless (u[UHost] && strlen(u[UHost])) {
|
|
croak("_failure_unsupported_notation_location",
|
|
"No support for fancy UNLs yet.");
|
|
QUIT
|
|
}
|
|
#ifdef NONIX
|
|
if (strlen(u[UResource]) > 1)
|
|
cvars["_INTERNAL_nick_target"] = u[UResource][1..];
|
|
#endif
|
|
P3(("DEBUG: is_localhost is %O for %O of %O\n",
|
|
is_localhost(u[UHost]), u[UHost], u))
|
|
unless (is_localhost(u[UHost])) {
|
|
if (trustworthy > 7) {
|
|
P1(("RELAYING permitted for %O to %O (%O)\n",
|
|
source, t, ME))
|
|
unless (cvars["_source_relay"])
|
|
cvars["_source_relay"] = source;
|
|
source = cvars["_source"] || query_server_unl();
|
|
// relay the message!
|
|
// this is used by procmail for example, whenever
|
|
// it needs to send to an xmpp: recipient.
|
|
sendmsg(t, mc, buffer==""? 0: buffer, cvars, source);
|
|
return restart();
|
|
}
|
|
P1(("RELAYING denied from %O to %O (%O)\n",
|
|
source, t, ME))
|
|
#if 0
|
|
monitor_report("_warning_unsupported_relay",
|
|
S("%O is trying to find %O here. Relaying denied.\n", ME, t));
|
|
croak("_failure_unsupported_relay",
|
|
//"Well done mate, you crashed me.");
|
|
"Relaying denied: [_host] is not a hostname of ours.",
|
|
([ "_host": u[UHost] ]));
|
|
// TODO: we quit here to not do the same hash-lookup
|
|
// in rootmsg again.
|
|
// (didn't get it? nevermind.. it's just el's sick humor)
|
|
QUIT
|
|
#endif
|
|
}
|
|
// .. yes.. add is_localhost check here, but without callback
|
|
// as we need a whitelist of allowed localhosts anyway..
|
|
// or would you like it if somebody added a CNAME stinkpfau.go
|
|
// to your server? no. are you going to blacklist him? no
|
|
// just like a webserver you will act depending on your configured
|
|
// hostnames, even if we let the end user configure it by specifying
|
|
// the hostname he likes his identities to be shown as...
|
|
// .. /set hostname psyc.duftepfau.go ... or was it /set id ?
|
|
//
|
|
// implement proxy service? it already exists for trustworthy senders
|
|
// do any checking for gateway schemes? TODO
|
|
if (u) o = find_psyc_object(u);
|
|
}
|
|
if (o) {
|
|
#ifdef PSYC_TCP
|
|
P2(("TCP[%s] => %O: %s\n", peeraddr, o || t, mc))
|
|
#else
|
|
PT(("UDP[%s] => %O: %s\n", peeraddr, o || t, mc));
|
|
#endif
|
|
P4(("%O's %s(%O) to %O\n", source, mc, buffer, o))
|
|
#ifdef TAGGING
|
|
// should we no longer use the monster sprintf to keep our callbacks, then
|
|
// the callbacks should in many cases ensure they got the reply from a
|
|
// legitimate sender.. or we'll have a most interesting mitm-zone here..
|
|
// then again, in some other cases, that is exactly what tags are for!
|
|
// FIXME: there is code for executing callbacks in
|
|
// entity.c - why is this here?
|
|
if (cvars["_tag_reply"] &&
|
|
objectp(o) && o->execute_callback(cvars["_tag_reply"],
|
|
({ source, mc, buffer == "" ? 0 : buffer, cvars }))) {
|
|
// NOOP
|
|
} else
|
|
#endif
|
|
if (objectp(o))
|
|
o -> msg(source, mc,
|
|
buffer == "" ? 0 : buffer, cvars, 0, t);
|
|
else
|
|
PP(("%O's %s(%O) to %O\n", source, mc, buffer, o));
|
|
#if 0
|
|
} else if (stringp(o) && strlen(o)) {
|
|
// should croak using sendmsg, huh?
|
|
croak("_error_unknown_target",
|
|
"No such object: [_target_given]",
|
|
([ "_target_given": t ]) );
|
|
// how does one refer to the incoming _target
|
|
// variable? _given doesnt look good to me.
|
|
// new: see _error_illegal_uniform below
|
|
#endif
|
|
#ifdef REPATCHING
|
|
return restart();
|
|
#else
|
|
return 1;
|
|
#endif
|
|
} else {
|
|
if (source) sendmsg(source, "_error_illegal_uniform",
|
|
"[_uniform] is not a legal address here.",
|
|
([ "_uniform": cvars["_target"],
|
|
"_tag_reply": cvars["_tag"] ]) );
|
|
else croak("_error_illegal_uniform",
|
|
"[_uniform] is not a legal address here.",
|
|
([ "_uniform": cvars["_target"],
|
|
"_tag_reply": cvars["_tag"] ]) );
|
|
}
|
|
}
|
|
if (context) {
|
|
#ifndef __PIKE__ // TPD
|
|
o = find_context(context);
|
|
unless(o) {
|
|
// no one is interested / online
|
|
// is this an abuse or a feature?
|
|
// fippo: feature, context may be 0
|
|
// if no one who is normally listening is online
|
|
return restart();
|
|
}
|
|
if (objectp(o))
|
|
o -> castmsg(source, mc,
|
|
buffer == "" ? 0 : buffer, cvars);
|
|
// this effectively ignores any _targets that may have
|
|
// been submitted with the message.. do we like that? TODO
|
|
#endif
|
|
return restart();
|
|
}
|
|
// else if (t) return _error_rejected_relay_outgoing TODO
|
|
t2 = query_server_unl();
|
|
// this part is new and maybe can be optimized.. TODO
|
|
// the main optimization would be to check for is_localhost
|
|
// with host part of target
|
|
if (t == t2 || (char_from_end(t2, 1) == '/' && t+"/" == t2)) {
|
|
// i know that this check sucks... but as we dont normalize
|
|
// uniforms...
|
|
// it would be much better to check if only host is set
|
|
// and then use is_localhost
|
|
sendmsg("/", mc, buffer == "" ? 0 : buffer, cvars, source);
|
|
return restart();
|
|
}
|
|
rootMsg(source, mc, buffer == "" ? 0 : buffer, cvars, t);
|
|
#ifdef REPATCHING
|
|
restart();
|
|
#endif
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
#ifdef PSYC_TCP
|
|
vamixed startParse(string a) {
|
|
if (a == ".") { // old S_GLYPH_PACKET_DELIMITER
|
|
restart();
|
|
# ifdef NOT_EXPERIMENTAL
|
|
if (isServer()) greet();
|
|
# endif
|
|
}
|
|
# ifdef SPYC_PATH
|
|
else if (a == "|") { // new S_GLYPH_PACKET_DELIMITER
|
|
object o = clone_object(SPYC_PATH "server");
|
|
unless (o && exec(o, ME) && o->logon(0)) {
|
|
croak("_failure_object_creation_server",
|
|
"Could not instantiate PSYC parser. Weird.");
|
|
QUIT
|
|
}
|
|
o->feed(a);
|
|
return 1;
|
|
}
|
|
# endif
|
|
# ifdef SWITCH2PSYC
|
|
// minor bug: we send the . right after <switched/> without a
|
|
// newline as xmpp doesn't require that and psyc doesn't want
|
|
// it. still ldmud puts the . in the wrong buffer, so let's
|
|
// just be tolerant and accept an empty newline for starting
|
|
// under this circumstance. however we should fix this one day.
|
|
else if (a == "") restart();
|
|
// how can we avoid the buffer from being re-input to the
|
|
// exec'd object? this however circumvents it
|
|
// in fact it seems to be a bug in older ldmud versions
|
|
// newer versions instead have the bug of providing an empty buffer
|
|
else if (abbrev("<?xml", a)) {
|
|
PT(("SWITCH2PSYC: re-encountered old buffer. ignoring: %O\n", a))
|
|
next_input_to(#'startParse);
|
|
return 1;
|
|
}
|
|
# endif
|
|
# ifdef MESSAGE_SEND_PROTOCOL
|
|
// only activate this if you use the PSYC port of lords
|
|
// minimal compliant MSP implementation ;)
|
|
// would love to code a bit more of it, but it is completely pointless
|
|
else if (a[0] == 'B' || a[0] == 'A') {
|
|
write("-MSP is of historic and cultural value, but please upgrade to PSYC.\n");
|
|
QUIT
|
|
}
|
|
#endif
|
|
else {
|
|
PT(("PSYC startParse got %O from %O\n", a, query_ip_number()))
|
|
croak("_error_syntax_initialization",
|
|
"The protocol begins with a dot on a line by itself.");
|
|
QUIT
|
|
}
|
|
#ifdef NOT_EXPERIMENTAL
|
|
pvars["_INTERNAL_source"] = "psyc://"+peeraddr+"/";
|
|
#endif
|
|
next_input_to(#'parse);
|
|
return 1;
|
|
}
|
|
#endif
|
|
|
|
string qOrigin() { return origin_unl; }
|
|
|
|
void sOrigin(string origin) { unless (origin_unl) origin_unl = origin; }
|
|
|
|
#endif /* !FORK */
|