Skip to content

Commit 6125cdc

Browse files
committed
updates to tls and http
1 parent 9c6b59e commit 6125cdc

4 files changed

Lines changed: 1327 additions & 10 deletions

File tree

lang/libs/http/src/client.ch

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -180,15 +180,38 @@ public namespace http {
180180
var default_timeout_secs: long;
181181
var max_response_header_bytes: usize;
182182
var max_body_len: usize; // 0 = unlimited streaming body (e.g. downloads); default 100MB
183+
// Optional TLS customization:
184+
// ca_chain - caller-owned trust anchor(s); used instead of the
185+
// system CA bundle for server certificate verification.
186+
// The Client does NOT take ownership (caller frees).
187+
// tls_skip_verify - disable certificate chain AND hostname verification
188+
// (self-signed local servers, test harnesses).
189+
var ca_chain: *mut tls::X509Cert;
190+
var tls_skip_verify: bool;
183191

184192
@constructor func constructor() {
185193
return Client {
186194
default_timeout_secs = 10,
187195
max_response_header_bytes = 64u * 1024u,
188-
max_body_len = DEFAULT_MAX_BODY_LEN
196+
max_body_len = DEFAULT_MAX_BODY_LEN,
197+
ca_chain = null,
198+
tls_skip_verify = false
189199
}
190200
}
191201

202+
// Use a specific CA certificate (chain) for HTTPS server verification
203+
// instead of the system bundle. The chain remains caller-owned.
204+
public func set_ca_chain(&mut self, ca: *mut tls::X509Cert) : &mut Client {
205+
ca_chain = ca;
206+
return self;
207+
}
208+
209+
// Disable TLS certificate and hostname verification (insecure mode).
210+
public func insecure_skip_verify(&mut self, skip: bool = true) : &mut Client {
211+
tls_skip_verify = skip;
212+
return self;
213+
}
214+
192215
public func request(&self, req_builder: &RequestBuilder) : std::Result<Response, std::string> {
193216
var is_https = req_builder.url.scheme.equals_with_len("https", 5)
194217
var s: net::Socket = 0
@@ -201,14 +224,24 @@ public namespace http {
201224
var ssl_ptr = malloc(sizeof(tls::SSLContext)) as *mut tls::SSLContext
202225
tls::ssl_init(ssl_ptr)
203226

204-
// Create config with auto-loaded CA. The config is heap-allocated
205-
// so it outlives this function (the Body's TLS context keeps
206-
// pointing at it); ssl_free releases it via conf_owned.
227+
// Create config with auto-loaded CA (or the caller's custom
228+
// chain). The config is heap-allocated so it outlives this
229+
// function (the Body's TLS context keeps pointing at it);
230+
// ssl_free releases it via conf_owned.
207231
var config = tls::ssl_config_init(tls::SSL_IS_CLIENT)
208-
var ca = tls::load_system_ca_bundle()
232+
var custom_ca = ca_chain != null
233+
var ca: *mut tls::X509Cert = null
234+
if(custom_ca) {
235+
ca = ca_chain
236+
} else {
237+
ca = tls::load_system_ca_bundle()
238+
}
209239
if(ca != null) {
210240
tls::ssl_set_ca_chain(&raw mut config, ca)
211241
}
242+
if(tls_skip_verify) {
243+
config.authmode = tls::SSL_VERIFY_NONE
244+
}
212245
var config_mem = malloc(sizeof(tls::SSLConfig)) as *mut tls::SSLConfig
213246
if(config_mem == null) {
214247
if(ca != null) { tls::cert_chain_free(ca) }
@@ -228,18 +261,19 @@ public namespace http {
228261
req_builder.url.host.data(),
229262
req_builder.url.port)
230263
if(ret < 0) {
231-
if(ca != null) { tls::cert_chain_free(ca) }
264+
if(ca != null && !custom_ca) { tls::cert_chain_free(ca) }
232265
tls::ssl_free(ssl_ptr)
233266
unsafe { dealloc ssl_ptr }
234267
return std::Result.Err<Response, std::string>(std::string::make_no_len("TLS handshake failed"))
235268
}
236269

237-
// CA bundle is only needed for handshake-time certificate
238-
// verification, which has completed — release it now.
239-
if(ca != null) { tls::cert_chain_free(ca) }
270+
// The CA bundle is only needed for handshake-time certificate
271+
// verification, which has completed — release it now (only if
272+
// we loaded it ourselves; custom chains are caller-owned).
273+
if(ca != null && !custom_ca) { tls::cert_chain_free(ca) }
240274

241275
// Verify the server's certificate matches the requested hostname
242-
if(ssl_ptr.peer_cert != null) {
276+
if(!tls_skip_verify && ssl_ptr.peer_cert != null) {
243277
var hostname_nul = std::string(req_builder.url.host.data(), req_builder.url.host.size())
244278
hostname_nul.append('\0')
245279
var hret = tls::x509_verify_hostname(ssl_ptr.peer_cert,

lang/tests/tls/src/helpers.ch

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,135 @@ func write_tls_python_utils() {
572572
py.append_view(" except Exception:\n")
573573
py.append_view(" pass\n")
574574
py.append_view(" s.close()\n")
575+
py.append_view("def read_http_request(c):\n")
576+
py.append_view(" data=b''\n")
577+
py.append_view(" while b'\\r\\n\\r\\n' not in data:\n")
578+
py.append_view(" d=c.recv(4096)\n")
579+
py.append_view(" if not d:return None,None,None\n")
580+
py.append_view(" data+=d\n")
581+
py.append_view(" head,rest=data.split(b'\\r\\n\\r\\n',1)\n")
582+
py.append_view(" lines=head.split(b'\\r\\n')\n")
583+
py.append_view(" parts=lines[0].split(b' ')\n")
584+
py.append_view(" method=parts[0].decode();path=parts[1].decode() if len(parts)>1 else '/'\n")
585+
py.append_view(" hdrs={}\n")
586+
py.append_view(" for h in lines[1:]:\n")
587+
py.append_view(" if b':' in h:\n")
588+
py.append_view(" k,v=h.split(b':',1);hdrs[k.strip().lower().decode()]=v.strip().decode()\n")
589+
py.append_view(" clen=int(hdrs.get('content-length','0'))\n")
590+
py.append_view(" while len(rest)<clen:\n")
591+
py.append_view(" d=c.recv(65536)\n")
592+
py.append_view(" if not d:break\n")
593+
py.append_view(" rest+=d\n")
594+
py.append_view(" return (method,path,hdrs,rest[:clen])\n")
595+
py.append_view("def httpsrv(cert,key,port,nconn=8):\n")
596+
py.append_view(" ctx=ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)\n")
597+
py.append_view(" ctx.load_cert_chain(cert,key)\n")
598+
py.append_view(" s=socket.socket()\n")
599+
py.append_view(" s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)\n")
600+
py.append_view(" s.bind(('127.0.0.1',int(port)))\n")
601+
py.append_view(" s.listen(int(nconn))\n")
602+
py.append_view(" s.settimeout(60)\n")
603+
py.append_view(" for i in range(int(nconn)):\n")
604+
py.append_view(" try:\n")
605+
py.append_view(" c,a=s.accept()\n")
606+
py.append_view(" except Exception as e:\n")
607+
py.append_view(" print('HTTPS_SRV_ACCEPT_ERR:'+str(e),file=sys.stderr)\n")
608+
py.append_view(" break\n")
609+
py.append_view(" try:\n")
610+
py.append_view(" t=ctx.wrap_socket(c,server_side=True)\n")
611+
py.append_view(" m,p,h,b=read_http_request(t)\n")
612+
py.append_view(" if m is None:t.close();continue\n")
613+
py.append_view(" if p.startswith('/status/'):\n")
614+
py.append_view(" code=int(p.split('/')[2]);body=b's';hdr={'Content-Length':'1'}\n")
615+
py.append_view(" elif p=='/hello' and m in ('GET','HEAD'):\n")
616+
py.append_view(" code=200;body=b'world';hdr={'Content-Length':'5','X-Custom':'chemical-test','Content-Type':'text/plain'}\n")
617+
py.append_view(" elif p=='/echo' and m=='POST':\n")
618+
py.append_view(" code=200;body=b;hdr={'Content-Length':str(len(b)),'X-Content-Type':h.get('content-type','')}\n")
619+
py.append_view(" elif p.startswith('/size/'):\n")
620+
py.append_view(" n=int(p.split('/')[2]);code=200;body=bytes([j%251 for j in range(n)]);hdr={'Content-Length':str(n),'Content-Type':'application/octet-stream'}\n")
621+
py.append_view(" elif p.startswith('/query'):\n")
622+
py.append_view(" q=p.split('?',1)[1] if '?' in p else '';code=200;body=q.encode();hdr={'Content-Length':str(len(body))}\n")
623+
py.append_view(" elif p=='/hdrs':\n")
624+
py.append_view(" body=(h.get('x-test','')+'|'+h.get('user-agent','')).encode();code=200\n")
625+
py.append_view(" hdr={'Content-Length':str(len(body))}\n")
626+
py.append_view(" elif p=='/chunked':\n")
627+
py.append_view(" payload=b'chunked-body-data'\n")
628+
py.append_view(" resp=b'HTTP/1.1 200 OK\\r\\nTransfer-Encoding: chunked\\r\\n\\r\\n'\n")
629+
py.append_view(" resp+=format(len(payload)//2,'x').encode()+b'\\r\\n'+payload[:len(payload)//2]+b'\\r\\n'\n")
630+
py.append_view(" resp+=format(len(payload)-len(payload)//2,'x').encode()+b'\\r\\n'+payload[len(payload)//2:]+b'\\r\\n0\\r\\n\\r\\n'\n")
631+
py.append_view(" t.sendall(resp);t.close();continue\n")
632+
py.append_view(" elif p=='/put' and m=='PUT':\n")
633+
py.append_view(" code=200;body=b'put:'+b;hdr={'Content-Length':str(len(body))}\n")
634+
py.append_view(" elif p=='/patch' and m=='PATCH':\n")
635+
py.append_view(" code=200;body=b'patched';hdr={'Content-Length':'7'}\n")
636+
py.append_view(" elif p=='/del' and m=='DELETE':\n")
637+
py.append_view(" code=200;body=b'deleted';hdr={'Content-Length':'7'}\n")
638+
py.append_view(" else:\n")
639+
py.append_view(" code=404;body=b'not found';hdr={'Content-Length':'9'}\n")
640+
py.append_view(" resp=('HTTP/1.1 '+str(code)+' T\\r\\n').encode()\n")
641+
py.append_view(" for k,v in hdr.items():resp+=(k+': '+v+'\\r\\n').encode()\n")
642+
py.append_view(" resp+=b'Connection: close\\r\\n\\r\\n'+body\n")
643+
py.append_view(" t.sendall(resp)\n")
644+
py.append_view(" t.close()\n")
645+
py.append_view(" except Exception as e:\n")
646+
py.append_view(" print('HTTPS_SRV_ERR:'+str(e),file=sys.stderr)\n")
647+
py.append_view(" s.close()\n")
648+
py.append_view("def hcli(host,port,outfile,mode,cafile=None,ciphers=None):\n")
649+
py.append_view(" import http.client,hashlib,time\n")
650+
py.append_view(" try:\n")
651+
py.append_view(" if cafile:\n")
652+
py.append_view(" cc=ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT);cc.load_verify_locations(cafile)\n")
653+
py.append_view(" else:\n")
654+
py.append_view(" cc=ssl._create_unverified_context()\n")
655+
py.append_view(" if ciphers:cc.set_ciphers(ciphers)\n")
656+
py.append_view(" ok=False;why=''\n")
657+
py.append_view(" if mode=='get':\n")
658+
py.append_view(" cn=http.client.HTTPSConnection(host,int(port),context=cc,timeout=15)\n")
659+
py.append_view(" cn.request('GET','/integrate');r=cn.getresponse();d=r.read()\n")
660+
py.append_view(" ok=(r.status==200 and d==b'SRV_OK');why='status=%s body=%r'%(r.status,d[:40])\n")
661+
py.append_view(" cn.close()\n")
662+
py.append_view(" elif mode=='post':\n")
663+
py.append_view(" payload=bytes([j%251 for j in range(50000)])\n")
664+
py.append_view(" cn=http.client.HTTPSConnection(host,int(port),context=cc,timeout=30)\n")
665+
py.append_view(" cn.request('POST','/integrate',body=payload,headers={'Content-Type':'application/octet-stream'})\n")
666+
py.append_view(" r=cn.getresponse();d=r.read()\n")
667+
py.append_view(" ok=(r.status==200 and d==payload);why='status=%s len=%d'%(r.status,len(d))\n")
668+
py.append_view(" cn.close()\n")
669+
py.append_view(" elif mode=='slowpost':\n")
670+
py.append_view(" payload=bytes([(j*7)%251 for j in range(20000)])\n")
671+
py.append_view(" s=socket.create_connection((host,int(port)),timeout=20)\n")
672+
py.append_view(" ts=cc.wrap_socket(s,server_hostname=host)\n")
673+
py.append_view(" head=b'POST /integrate HTTP/1.1\\r\\nHost: '+host.encode()+b'\\r\\nContent-Length: 20000\\r\\nConnection: close\\r\\n\\r\\n'\n")
674+
py.append_view(" ts.sendall(head)\n")
675+
py.append_view(" off=0\n")
676+
py.append_view(" while off<len(payload):\n")
677+
py.append_view(" ts.sendall(payload[off:off+1024]);time.sleep(0.01);off+=1024\n")
678+
py.append_view(" buf=b''\n")
679+
py.append_view(" while True:\n")
680+
py.append_view(" d=ts.recv(65536)\n")
681+
py.append_view(" if not d:break\n")
682+
py.append_view(" buf+=d\n")
683+
py.append_view(" ts.close()\n")
684+
py.append_view(" body=buf.split(b'\\r\\n\\r\\n',1)[1] if b'\\r\\n\\r\\n' in buf else b''\n")
685+
py.append_view(" ok=(body==payload);why='resp_len=%d'%len(body)\n")
686+
py.append_view(" elif mode=='keepalive':\n")
687+
py.append_view(" cn=http.client.HTTPSConnection(host,int(port),context=cc,timeout=15)\n")
688+
py.append_view(" cn.request('GET','/integrate?a=1');r1=cn.getresponse();d1=r1.read()\n")
689+
py.append_view(" cn.request('GET','/integrate?a=2');r2=cn.getresponse();d2=r2.read()\n")
690+
py.append_view(" ok=(r1.status==200 and d1==b'SRV_OK:a=1' and r2.status==200 and d2==b'SRV_OK:a=2')\n")
691+
py.append_view(" why='r1=%s/%r r2=%s/%r'%(r1.status,d1,r2.status,d2)\n")
692+
py.append_view(" cn.close()\n")
693+
py.append_view(" elif mode=='bigresp':\n")
694+
py.append_view(" cn=http.client.HTTPSConnection(host,int(port),context=cc,timeout=60)\n")
695+
py.append_view(" cn.request('GET','/big');r=cn.getresponse();d=r.read()\n")
696+
py.append_view(" want=bytes([j%251 for j in range(300000)])\n")
697+
py.append_view(" ok=(r.status==200 and d==want);why='status=%s len=%d'%(r.status,len(d))\n")
698+
py.append_view(" cn.close()\n")
699+
py.append_view(" f=open(outfile,'w')\n")
700+
py.append_view(" f.write(('RESULT:OK' if ok else 'RESULT:FAIL '+why))\n")
701+
py.append_view(" f.close()\n")
702+
py.append_view(" except Exception as e:\n")
703+
py.append_view(" f=open(outfile,'w');f.write('RESULT:FAIL exception '+str(e));f.close()\n")
575704
py.append_view("cmd=sys.argv[1]\n")
576705
py.append_view("if cmd=='cert':\n")
577706
py.append_view(" gen_cert(sys.argv[2],sys.argv[3],sys.argv[4],sys.argv[5] if len(sys.argv)>5 else 'ec')\n")
@@ -602,5 +731,9 @@ func write_tls_python_utils() {
602731
py.append_view(" echo_srv(sys.argv[2],sys.argv[3],sys.argv[4],sys.argv[5] if len(sys.argv)>5 else 1,sys.argv[6] if len(sys.argv)>6 else 16384)\n")
603732
py.append_view("elif cmd=='plaintcp':\n")
604733
py.append_view(" plaintcp(sys.argv[2],sys.argv[3],sys.argv[4] if len(sys.argv)>4 else 1)\n")
734+
py.append_view("elif cmd=='httpsrv':\n")
735+
py.append_view(" httpsrv(sys.argv[2],sys.argv[3],sys.argv[4],sys.argv[5] if len(sys.argv)>5 else 8)\n")
736+
py.append_view("elif cmd=='hcli':\n")
737+
py.append_view(" hcli(sys.argv[2],sys.argv[3],sys.argv[4],sys.argv[5] if len(sys.argv)>5 else 'get',sys.argv[6] if len(sys.argv)>6 and sys.argv[6]!='-' else None,sys.argv[7] if len(sys.argv)>7 and sys.argv[7]!='-' else None)\n")
605738
test_write_file("/tmp/tls_utils.py\0" as *char, py.data() as *u8, py.size())
606739
}

0 commit comments

Comments
 (0)