adam@14: #include adam@1: #include adam@1: adam@6: #include adam@6: #include adam@6: #include adam@0: #include adam@7: #include adam@8: #include adam@0: #include adam@1: #include adam@0: adam@26: #include adam@26: adam@1: #include adam@0: adam@3: #define BUF_MAX 10240 adam@3: #define BUF_INIT 1024 adam@3: adam@26: #define DEFAULT_PRIME "DCF93A0B883972EC0E19989AC5A2CE310E1D37717E8D9571BB7623731866E61EF75A2E27898B057F9891C2E27A639C3F29B60814581CD3B2CA3986D2683705577D45C2E7E52DC81C7A171876E5CEA74B1448BFDFAF18828EFD2519F14E45E3826634AF1949E5B535CC829A483B8A76223E5D490A257F05BDFF16F2FB22C583AB" adam@26: #define DEFAULT_GENERATOR "2" adam@26: adam@26: static BIGNUM *default_prime, *default_generator; adam@2: adam@2: uw_Basis_string uw_OpenidFfi_endpoint(uw_context ctx, uw_OpenidFfi_discovery d) { adam@8: return d.endpoint; adam@2: } adam@2: adam@2: uw_Basis_string uw_OpenidFfi_localId(uw_context ctx, uw_OpenidFfi_discovery d) { adam@8: return d.localId; adam@2: } adam@2: adam@26: static pthread_mutex_t *locks; adam@26: adam@26: static void locking_function(int mode, int n, const char *file, int line) { adam@26: if (mode & CRYPTO_LOCK) adam@26: pthread_mutex_lock(&locks[n]); adam@26: else adam@26: pthread_mutex_unlock(&locks[n]); adam@26: } adam@26: adam@0: uw_unit uw_OpenidFfi_init(uw_context ctx) { adam@26: int nl = CRYPTO_num_locks(), i; adam@26: locks = malloc(sizeof(pthread_mutex_t) * nl); adam@26: for (i = 0; i < nl; ++i) adam@26: pthread_mutex_init(&locks[i], NULL); adam@26: adam@26: CRYPTO_set_locking_callback(locking_function); adam@0: curl_global_init(CURL_GLOBAL_ALL); adam@0: adam@26: BN_hex2bn(&default_prime, DEFAULT_PRIME); adam@26: BN_dec2bn(&default_generator, DEFAULT_GENERATOR); adam@26: adam@0: return uw_unit_v; adam@0: } adam@1: adam@1: static CURL *curl(uw_context ctx) { adam@1: CURL *r; adam@1: adam@1: if (!(r = uw_get_global(ctx, "curl"))) { adam@1: r = curl_easy_init(); adam@1: if (r) adam@1: uw_set_global(ctx, "curl", r, curl_easy_cleanup); adam@1: } adam@1: adam@1: return r; adam@1: } adam@1: adam@26: typedef enum { NONE, SERVICE, TYPE, MATCHED, URI } xrds_mode; adam@26: adam@1: typedef struct { adam@1: uw_context ctx; adam@8: uw_OpenidFfi_discovery *d; adam@26: xrds_mode mode; adam@42: int cur_priority, max_priority, found_old_version; adam@1: } endpoint; adam@1: adam@1: static void XMLCALL startElement(void *userData, const XML_Char *name, const XML_Char **atts) { adam@1: endpoint *ep = userData; adam@1: adam@26: if (!strncmp(name, "xrd:", 4)) adam@26: name += 4; adam@26: adam@1: if (!strcmp(name, "link")) { adam@1: const XML_Char **attp; adam@1: int found = 0; adam@1: adam@1: for (attp = atts; *attp; attp += 2) { adam@1: if (!strcmp(attp[0], "rel") && !strcmp(attp[1], "openid2.provider")) { adam@1: found = 1; adam@1: break; adam@42: } else if (!strcmp(attp[0], "rel") && !strcmp(attp[1], "openid.server")) { adam@42: ep->found_old_version = 1; adam@42: return; adam@1: } adam@1: } adam@1: adam@1: if (found) { adam@1: for (attp = atts; *attp; attp += 2) { adam@1: if (!strcmp(attp[0], "href")) { adam@2: ep->d->endpoint = uw_strdup(ep->ctx, attp[1]); adam@1: return; adam@1: } adam@1: } adam@1: } adam@1: } adam@27: else if (!strcmp(name, "Service")) { adam@27: const XML_Char **attp; adam@27: adam@27: ep->cur_priority = 0; adam@27: for (attp = atts; *attp; attp += 2) adam@27: if (!strcmp(attp[0], "priority")) { adam@27: ep->cur_priority = atoi(attp[1]); adam@27: break; adam@27: } adam@27: adam@26: ep->mode = SERVICE; adam@27: } else if (!strcmp(name, "Type")) { adam@26: if (ep->mode == SERVICE) adam@26: ep->mode = TYPE; adam@26: } adam@26: else if (!strcmp(name, "URI")) { adam@26: if (ep->mode == MATCHED) adam@26: ep->mode = URI; adam@26: } adam@26: } adam@26: adam@26: static char server[] = "http://specs.openid.net/auth/2.0/server"; adam@26: static char signon[] = "http://specs.openid.net/auth/2.0/signon"; adam@26: adam@26: static void XMLCALL cdata(void *userData, const XML_Char *s, int len) { adam@26: endpoint *ep = userData; adam@26: adam@26: switch (ep->mode) { adam@26: case TYPE: adam@26: if ((len == sizeof(server)-1 && !memcmp(server, s, sizeof(server)-1)) adam@26: || (len == sizeof(signon)-1 && !memcmp(signon, s, sizeof(signon)-1))) adam@26: ep->mode = MATCHED; adam@26: break; adam@26: case URI: adam@27: if (ep->cur_priority < ep->max_priority) { adam@27: ep->d->endpoint = uw_malloc(ep->ctx, len+1); adam@27: memcpy(ep->d->endpoint, s, len); adam@27: ep->d->endpoint[len] = 0; adam@27: ep->max_priority = ep->cur_priority; adam@27: } adam@26: break; adam@26: default: adam@26: break; adam@26: } adam@26: } adam@26: adam@26: static void XMLCALL endElement(void *userData, const XML_Char *name) { adam@26: endpoint *ep = userData; adam@26: adam@26: if (!strncmp(name, "xrd:", 4)) adam@26: name += 4; adam@26: adam@26: if (!strcmp(name, "Service")) adam@26: ep->mode = NONE; adam@26: else if (!strcmp(name, "Type")) { adam@26: if (ep->mode != MATCHED) adam@26: ep->mode = SERVICE; adam@26: } adam@26: else if (!strcmp(name, "URI")) adam@26: ep->mode = MATCHED; adam@1: } adam@1: adam@1: typedef struct { adam@1: XML_Parser parser; adam@1: int any_errors; adam@3: } curl_discovery_data; adam@1: adam@3: static size_t write_discovery_data(void *buffer, size_t size, size_t nmemb, void *userp) { adam@3: curl_discovery_data *d = userp; adam@1: adam@1: if (!XML_Parse(d->parser, buffer, size * nmemb, 0)) adam@1: d->any_errors = 1; adam@1: adam@1: return size * nmemb; adam@1: } adam@1: adam@2: uw_OpenidFfi_discovery *uw_OpenidFfi_discover(uw_context ctx, uw_Basis_string id) { adam@1: char *s; adam@1: CURL *c = curl(ctx); adam@3: curl_discovery_data cd = {}; adam@8: uw_OpenidFfi_discovery *dy = uw_malloc(ctx, sizeof(uw_OpenidFfi_discovery)); adam@42: endpoint ep = {ctx, dy, NONE, 0, INT_MAX, 0}; adam@1: CURLcode code; adam@27: struct curl_slist *headers = NULL; adam@1: adam@2: dy->endpoint = dy->localId = NULL; adam@2: adam@1: if (!strchr(id, ':')) { adam@1: id = uw_Basis_strcat(ctx, "http://", id); adam@1: if ((s = strchr(id, '#')) != NULL) adam@1: *s = 0; adam@1: } else if ((s = strchr(id, '#')) != NULL) { adam@1: char *id2 = uw_malloc(ctx, s - id + 1); adam@1: memcpy(id2, s, s - id); adam@1: id2[s - id] = 0; adam@1: id = id2; adam@1: } adam@1: adam@1: cd.parser = XML_ParserCreate(NULL); adam@1: XML_SetUserData(cd.parser, &ep); adam@1: uw_push_cleanup(ctx, (void (*)(void *))XML_ParserFree, cd.parser); adam@26: XML_SetElementHandler(cd.parser, startElement, endElement); adam@26: XML_SetCharacterDataHandler(cd.parser, cdata); adam@1: adam@26: curl_easy_reset(c); adam@40: curl_easy_setopt(c, CURLOPT_SSLVERSION, CURL_SSLVERSION_SSLv3); adam@1: curl_easy_setopt(c, CURLOPT_URL, id); adam@3: curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, write_discovery_data); adam@1: curl_easy_setopt(c, CURLOPT_WRITEDATA, &cd); adam@27: curl_slist_append(headers, "Accept: application/xrds+xml"); adam@27: uw_push_cleanup(ctx, (void (*)(void *))curl_slist_free_all, headers); adam@27: curl_easy_setopt(c, CURLOPT_HTTPHEADER, headers); adam@1: adam@1: code = curl_easy_perform(c); adam@1: uw_pop_cleanup(ctx); adam@27: uw_pop_cleanup(ctx); adam@1: adam@40: if (code) { adam@40: uw_Basis_debug(ctx, "CURL error:"); adam@40: uw_Basis_debug(ctx, (char*)curl_easy_strerror(code)); adam@40: } adam@42: else if (!dy->endpoint) { adam@42: if (ep.found_old_version) adam@42: uw_Basis_debug(ctx, "Provider doesn't support OpenID version 2.0 but does support an older version"); adam@42: else adam@42: uw_Basis_debug(ctx, "Couldn't parse endpoint from page"); adam@42: } adam@40: adam@8: if (code || !dy->endpoint) adam@1: return NULL; adam@8: else adam@8: return dy; adam@1: } adam@3: adam@3: uw_OpenidFfi_inputs uw_OpenidFfi_createInputs(uw_context ctx) { adam@3: uw_buffer *r = uw_malloc(ctx, sizeof(uw_buffer)); adam@3: uw_buffer_init(BUF_MAX, r, BUF_INIT); adam@3: return r; adam@3: } adam@3: adam@8: static void postify(uw_OpenidFfi_inputs buf, uw_Basis_string s) { adam@14: char hex[4]; adam@14: adam@8: for (; *s; ++s) { adam@14: if (isalnum(*s)) adam@14: uw_buffer_append(buf, s, 1); adam@14: else { adam@14: sprintf(hex, "%%%02X", (unsigned char)*s); adam@14: uw_buffer_append(buf, hex, 3); adam@14: } adam@8: } adam@3: } adam@3: adam@3: uw_unit uw_OpenidFfi_addInput(uw_context ctx, uw_OpenidFfi_inputs buf, uw_Basis_string key, uw_Basis_string value) { adam@3: if (uw_buffer_used(buf) > 0) adam@3: uw_buffer_append(buf, "&", 1); adam@3: adam@14: uw_buffer_append(buf, key, strlen(key)); adam@3: uw_buffer_append(buf, "=", 1); adam@8: postify(buf, value); adam@3: adam@3: return uw_unit_v; adam@3: } adam@3: adam@3: uw_Basis_string uw_OpenidFfi_getOutput(uw_context ctx, uw_OpenidFfi_outputs buf, uw_Basis_string key) { adam@3: char *s = buf->start; adam@3: adam@3: for (; *s; s = strchr(strchr(s, 0)+1, 0)+1) adam@3: if (!strcmp(key, s)) adam@3: return strchr(s, 0)+1; adam@3: adam@3: return NULL; adam@3: } adam@3: adam@27: uw_unit uw_OpenidFfi_printOutputs(uw_context ctx, uw_OpenidFfi_outputs buf) { adam@27: char *s = buf->start; adam@27: adam@27: for (; *s; s = strchr(strchr(s, 0)+1, 0)+1) adam@27: fprintf(stderr, "%s => %s\n", s, strchr(s, 0)+1); adam@27: adam@27: return uw_unit_v; adam@27: } adam@27: adam@3: static size_t write_buffer_data(void *buffer, size_t size, size_t nmemb, void *userp) { adam@3: uw_buffer *buf = userp; adam@3: adam@3: uw_buffer_append(buf, buffer, size * nmemb); adam@3: adam@3: return size * nmemb; adam@3: } adam@3: adam@3: const char curl_failure[] = "error\0Error fetching URL"; adam@3: adam@4: uw_OpenidFfi_outputs uw_OpenidFfi_direct(uw_context ctx, uw_Basis_string url, uw_OpenidFfi_inputs inps) { adam@3: uw_buffer *buf = uw_malloc(ctx, sizeof(uw_buffer)); adam@3: CURL *c = curl(ctx); adam@3: CURLcode code; adam@3: adam@3: uw_buffer_init(BUF_MAX, buf, BUF_INIT); adam@3: adam@3: uw_buffer_append(inps, "", 1); adam@3: adam@26: curl_easy_reset(c); adam@40: curl_easy_setopt(c, CURLOPT_SSLVERSION, CURL_SSLVERSION_SSLv3); adam@3: curl_easy_setopt(c, CURLOPT_URL, url); adam@3: curl_easy_setopt(c, CURLOPT_POSTFIELDS, inps->start); adam@3: curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, write_buffer_data); adam@3: curl_easy_setopt(c, CURLOPT_WRITEDATA, buf); adam@3: adam@3: code = curl_easy_perform(c); adam@3: uw_buffer_append(buf, "", 1); adam@3: adam@3: if (code) { adam@3: uw_buffer_reset(buf); adam@3: uw_buffer_append(buf, curl_failure, sizeof curl_failure); adam@3: } else { adam@3: char *s; adam@3: adam@3: s = buf->start; adam@3: while (*s) { adam@3: char *colon = strchr(s, ':'), *newline; adam@3: adam@3: if (!colon) { adam@3: *s = 0; adam@3: break; adam@3: } adam@3: adam@7: *colon = 0; adam@7: adam@3: newline = strchr(colon+1, '\n'); adam@3: adam@7: if (!newline) adam@3: break; adam@3: adam@3: *newline = 0; adam@3: s = newline+1; adam@3: } adam@3: } adam@3: adam@3: return buf; adam@3: } adam@4: adam@4: static uw_Basis_string deurl(uw_context ctx, uw_Basis_string s) { adam@4: uw_Basis_string r = uw_malloc(ctx, strlen(s)), s2 = r; adam@4: adam@4: for (; *s; ++s) { adam@4: if (s[0] == '%' && s[1] && s[2]) { adam@4: unsigned u; adam@4: adam@4: sscanf(s+1, "%02x", &u); adam@4: *s2++ = u; adam@4: s += 2; adam@4: } else adam@4: *s2++ = *s; adam@4: } adam@4: adam@4: *s2 = 0; adam@4: return r; adam@4: } adam@4: adam@4: uw_OpenidFfi_outputs uw_OpenidFfi_indirect(uw_context ctx, uw_Basis_string fields) { adam@4: uw_OpenidFfi_outputs b = malloc(sizeof(uw_buffer)); adam@4: adam@4: uw_buffer_init(BUF_MAX, b, BUF_INIT); adam@4: adam@6: fields = uw_strdup(ctx, fields); adam@6: adam@4: while (*fields) { adam@4: char *equal = strchr(fields, '='), *and, *s; adam@4: adam@4: if (!equal) adam@4: break; adam@4: adam@4: *equal = 0; adam@4: s = deurl(ctx, fields); adam@4: uw_buffer_append(b, s, strlen(s)); adam@4: uw_buffer_append(b, "", 1); adam@4: adam@4: and = strchr(equal+1, '&'); adam@4: if (and) { adam@4: *and = 0; adam@4: fields = and+1; adam@4: } else adam@4: fields = and = strchr(equal+1, 0); adam@4: s = deurl(ctx, equal+1); adam@4: uw_buffer_append(b, s, strlen(s)); adam@4: uw_buffer_append(b, "", 1); adam@4: } adam@4: adam@4: uw_buffer_append(b, "", 1); adam@4: return b; adam@4: } adam@6: adam@6: static uw_Basis_string base64(uw_context ctx, unsigned char *input, int length) { adam@6: BIO *bmem, *b64; adam@6: adam@6: b64 = BIO_new(BIO_f_base64()); adam@7: BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); adam@6: bmem = BIO_new(BIO_s_mem()); adam@7: BIO_push(b64, bmem); adam@6: BIO_write(b64, input, length); adam@6: (void)BIO_flush(b64); adam@6: adam@7: int len = BIO_ctrl_pending(bmem); adam@7: char *buff = uw_malloc(ctx, len+1); adam@7: BIO_read(bmem, buff, len); adam@7: buff[len] = 0; adam@6: adam@6: BIO_free_all(b64); adam@6: adam@6: return buff; adam@6: } adam@6: adam@8: static int unbase64(unsigned char *input, int length, unsigned char *buffer, int bufferLength) adam@7: { adam@7: BIO *b64, *bmem; adam@8: int n; adam@6: adam@7: b64 = BIO_new(BIO_f_base64()); adam@7: BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); adam@7: bmem = BIO_new_mem_buf(input, length); adam@7: BIO_push(b64, bmem); adam@8: n = BIO_read(b64, buffer, bufferLength); adam@6: adam@41: BIO_free_all(b64); adam@8: adam@8: return n; adam@8: } adam@8: adam@12: uw_Basis_string uw_OpenidFfi_hmac_sha1(uw_context ctx, uw_Basis_string key, uw_Basis_string data) { adam@8: unsigned char keyBin[SHA_DIGEST_LENGTH], out[EVP_MAX_MD_SIZE]; adam@8: unsigned outLen; adam@8: adam@8: unbase64((unsigned char *)key, strlen(key), keyBin, sizeof keyBin); adam@8: adam@8: HMAC(EVP_sha1(), keyBin, sizeof keyBin, (unsigned char *)data, strlen(data), out, &outLen); adam@8: return base64(ctx, out, outLen); adam@6: } adam@7: adam@12: uw_Basis_string uw_OpenidFfi_hmac_sha256(uw_context ctx, uw_Basis_string key, uw_Basis_string data) { adam@7: unsigned char keyBin[SHA256_DIGEST_LENGTH], out[EVP_MAX_MD_SIZE]; adam@7: unsigned outLen; adam@7: adam@7: unbase64((unsigned char *)key, strlen(key), keyBin, sizeof keyBin); adam@7: adam@7: HMAC(EVP_sha256(), keyBin, sizeof keyBin, (unsigned char *)data, strlen(data), out, &outLen); adam@7: return base64(ctx, out, outLen); adam@7: } adam@8: adam@8: static uw_Basis_string btwoc(uw_context ctx, const BIGNUM *n) { adam@8: int len = BN_num_bytes(n), i; adam@8: unsigned char bytes[len+1]; adam@8: adam@8: bytes[0] = 0; adam@8: BN_bn2bin(n, bytes+1); adam@8: adam@8: for (i = 1; i <= len; ++i) adam@8: if (bytes[i]) { adam@8: if (bytes[i] & 0x80) adam@8: --i; adam@8: break; adam@8: } adam@8: adam@8: if (i > len) adam@8: i = len; adam@8: adam@8: return base64(ctx, bytes+i, len+1-i); adam@8: } adam@8: adam@8: static BIGNUM *unbtwoc(uw_context ctx, uw_Basis_string s) { adam@8: unsigned char bytes[1024]; adam@8: int len; adam@8: adam@8: len = unbase64((unsigned char *)s, strlen(s), bytes, sizeof bytes); adam@8: return BN_bin2bn(bytes, len, NULL); adam@8: } adam@8: adam@8: uw_Basis_string uw_OpenidFfi_modulus(uw_context ctx, uw_OpenidFfi_dh dh) { adam@8: return btwoc(ctx, dh->p); adam@8: } adam@8: adam@8: uw_Basis_string uw_OpenidFfi_generator(uw_context ctx, uw_OpenidFfi_dh dh) { adam@8: return btwoc(ctx, dh->g); adam@8: } adam@8: adam@8: uw_Basis_string uw_OpenidFfi_public(uw_context ctx, uw_OpenidFfi_dh dh) { adam@8: return btwoc(ctx, dh->pub_key); adam@8: } adam@8: adam@8: static void free_DH(void *data, int will_retry) { adam@8: DH *dh = data; adam@26: dh->p = NULL; adam@26: dh->g = NULL; adam@8: DH_free(dh); adam@8: } adam@8: adam@8: uw_OpenidFfi_dh uw_OpenidFfi_generate(uw_context ctx) { adam@8: DH *dh = DH_new(); adam@8: adam@26: dh->p = default_prime; adam@26: dh->g = default_generator; adam@8: uw_register_transactional(ctx, dh, NULL, NULL, free_DH); adam@8: adam@8: if (DH_generate_key(dh) != 1) adam@8: uw_error(ctx, FATAL, "Diffie-Hellman key generation failed"); adam@8: adam@8: return dh; adam@8: } adam@8: adam@8: uw_Basis_string uw_OpenidFfi_compute(uw_context ctx, uw_OpenidFfi_dh dh, uw_Basis_string server_pub) { adam@8: BIGNUM *bn = unbtwoc(ctx, server_pub); adam@12: unsigned char secret[DH_size(dh)+1], *secretP; adam@8: int size; adam@8: adam@8: uw_push_cleanup(ctx, (void (*)(void *))BN_free, bn); adam@8: adam@12: size = DH_compute_key(secret+1, bn, dh); adam@8: if (size == -1) adam@8: uw_error(ctx, FATAL, "Diffie-Hellman key computation failed"); adam@8: adam@8: uw_pop_cleanup(ctx); adam@8: adam@12: if (size > 0 && (secret[1] & 0x80)) { adam@12: secret[0] = 0; adam@12: secretP = secret; adam@12: ++size; adam@12: } else adam@12: secretP = secret+1; adam@12: adam@12: return base64(ctx, secretP, size); adam@8: } adam@12: adam@12: uw_Basis_string uw_OpenidFfi_sha1(uw_context ctx, uw_Basis_string data) { adam@12: unsigned char dataBin[128], out[EVP_MAX_MD_SIZE]; adam@12: int len; adam@12: adam@12: len = unbase64((unsigned char *)data, strlen(data), dataBin, sizeof dataBin); adam@12: adam@12: SHA1(dataBin, len, out); adam@12: return base64(ctx, out, SHA_DIGEST_LENGTH); adam@12: } adam@12: adam@12: uw_Basis_string uw_OpenidFfi_sha256(uw_context ctx, uw_Basis_string data) { adam@12: unsigned char dataBin[128], out[EVP_MAX_MD_SIZE]; adam@12: int len; adam@12: adam@12: len = unbase64((unsigned char *)data, strlen(data), dataBin, sizeof dataBin); adam@12: adam@12: SHA256(dataBin, len, out); adam@12: return base64(ctx, out, SHA256_DIGEST_LENGTH); adam@12: } adam@12: adam@12: uw_Basis_string uw_OpenidFfi_xor(uw_context ctx, uw_Basis_string s1, uw_Basis_string s2) { adam@12: unsigned char buf1[128], buf2[128], bufO[128]; adam@12: int len1, len2, i; adam@12: adam@12: len1 = unbase64((unsigned char *)s1, strlen(s1), buf1, sizeof buf1); adam@12: len2 = unbase64((unsigned char *)s2, strlen(s2), buf2, sizeof buf2); adam@12: adam@12: for (i = 0; i < len1; ++i) adam@12: bufO[i] = buf1[i] ^ buf2[i % len2]; adam@12: adam@12: return base64(ctx, bufO, len1); adam@12: } adam@13: greenrd@43: uw_Basis_bool __attribute__((optimize(0))) uw_OpenidFfi_secCmp(uw_context ctx, uw_Basis_string s1, uw_Basis_string s2) { greenrd@43: int i, x = 0, len1 = strlen(s1); greenrd@43: if (len1 != strlen(s2)) return 0; greenrd@43: for (i = 0; i < len1; ++i) greenrd@43: x |= s1[i] ^ s2[i]; greenrd@43: return x == 0; greenrd@43: } greenrd@43: adam@13: uw_OpenidFfi_inputs uw_OpenidFfi_remode(uw_context ctx, uw_OpenidFfi_outputs out, uw_Basis_string mode) { adam@13: uw_OpenidFfi_inputs in = uw_OpenidFfi_createInputs(ctx); adam@13: char *s; adam@13: adam@13: for (s = out->start; *s; s = strchr(strchr(s, 0)+1, 0)+1) adam@13: if (!strcmp("openid.mode", s)) adam@13: uw_OpenidFfi_addInput(ctx, in, "openid.mode", mode); adam@13: else adam@13: uw_OpenidFfi_addInput(ctx, in, s, strchr(s, 0)+1); adam@13: adam@13: return in; adam@13: }