Fossil SCM

Rework the cgi_http_server() routine so that it uses two separate sockets, one each for IPv4 and IPv6.

drh 2025-04-18 12:23 trunk
Commit 945e0ae4eb57dac7b0bf72ef6fb8da5c8c9cb90934049b715add1e40b18d85e7
1 file changed +252 -187
+252 -187
--- src/cgi.c
+++ src/cgi.c
@@ -2497,29 +2497,10 @@
24972497
}
24982498
fossil_free(zToFree);
24992499
fgetc(g.httpIn); /* Read past the "," separating header from content */
25002500
cgi_init();
25012501
}
2502
-
2503
-#if !defined(_WIN32)
2504
-/*
2505
-** Change the listening socket, if necessary, so that it will accept both IPv4
2506
-** and IPv6
2507
-*/
2508
-static void allowBothIpV4andV6(int listener){
2509
-#if defined(IPV6_V6ONLY)
2510
- int ipv6only = -1;
2511
- socklen_t ipv6only_size = sizeof(ipv6only);
2512
- getsockopt(listener, IPPROTO_IPV6, IPV6_V6ONLY, &ipv6only, &ipv6only_size);
2513
- if( ipv6only ){
2514
- ipv6only = 0;
2515
- setsockopt(listener, IPPROTO_IPV6, IPV6_V6ONLY, &ipv6only, ipv6only_size);
2516
- }
2517
-#endif /* defined(IPV6_ONLY) */
2518
-}
2519
-#endif /* !defined(_WIN32) */
2520
-
25212502
25222503
#if INTERFACE
25232504
/*
25242505
** Bitmap values for the flags parameter to cgi_http_server().
25252506
*/
@@ -2559,150 +2540,222 @@
25592540
){
25602541
#if defined(_WIN32)
25612542
/* Use win32_http_server() instead */
25622543
fossil_exit(1);
25632544
#else
2564
- int listener = -1; /* The server socket */
2565
- int connection; /* A socket for each individual connection */
2545
+ int listen4 = -1; /* Main socket; IPv4 or unix-domain */
2546
+ int listen6 = -1; /* Aux socket for corresponding IPv6 */
2547
+ int mxListen = -1; /* Maximum of listen4 and listen6 */
2548
+ int connection; /* An incoming connection */
25662549
int nRequest = 0; /* Number of requests handled so far */
25672550
fd_set readfds; /* Set of file descriptors for select() */
25682551
socklen_t lenaddr; /* Length of the inaddr structure */
25692552
int child; /* PID of the child process */
25702553
int nchildren = 0; /* Number of child processes */
25712554
struct timeval delay; /* How long to wait inside select() */
2572
- struct sockaddr_in6 inaddr; /* The socket address */
2573
- struct sockaddr_in inaddr4; /* IPv4 address; needed by OpenBSD */
2555
+ struct sockaddr_in6 inaddr6; /* Address for IPv6 */
2556
+ struct sockaddr_in inaddr4; /* Address for IPv4 */
25742557
struct sockaddr_un uxaddr; /* The address for unix-domain sockets */
25752558
int opt = 1; /* setsockopt flag */
25762559
int rc; /* Result code from system calls */
25772560
int iPort = mnPort; /* Port to try to use */
2578
- int bIPv4 = 0; /* Use IPv4 only; use inaddr4, not inaddr */
2579
-
2580
- while( iPort<=mxPort ){
2581
- if( flags & HTTP_SERVER_UNIXSOCKET ){
2582
- /* Initialize a Unix socket named g.zSockName */
2583
- assert( g.zSockName!=0 );
2584
- memset(&uxaddr, 0, sizeof(uxaddr));
2585
- if( strlen(g.zSockName)>sizeof(uxaddr.sun_path) ){
2586
- fossil_fatal("name of unix socket too big: %s\nmax size: %d\n",
2587
- g.zSockName, (int)sizeof(uxaddr.sun_path));
2588
- }
2589
- if( file_isdir(g.zSockName, ExtFILE)!=0 ){
2590
- if( !file_issocket(g.zSockName) ){
2591
- fossil_fatal("cannot name socket \"%s\" because another object"
2592
- " with that name already exists", g.zSockName);
2593
- }else{
2594
- unlink(g.zSockName);
2595
- }
2596
- }
2597
- uxaddr.sun_family = AF_UNIX;
2598
- strncpy(uxaddr.sun_path, g.zSockName, sizeof(uxaddr.sun_path)-1);
2599
- listener = socket(AF_UNIX, SOCK_STREAM, 0);
2600
- if( listener<0 ){
2601
- fossil_fatal("unable to create a unix socket named %s",
2602
- g.zSockName);
2603
- }
2604
- /* Set the access permission for the new socket. Default to 0660.
2605
- ** But use an alternative specified by --socket-mode if available.
2606
- ** Do this before bind() to avoid a race condition. */
2607
- if( g.zSockMode ){
2608
- file_set_mode(g.zSockName, listener, g.zSockMode, 0);
2609
- }else{
2610
- file_set_mode(g.zSockName, listener, "0660", 1);
2611
- }
2612
- }else{
2613
- /* Initialize a TCP/IP socket on port iPort */
2614
- if( (flags & HTTP_SERVER_LOCALHOST)!=0 && zIpAddr==0 ){
2615
- /* Map all loopback to 127.0.0.1, since this is the easiest way
2616
- ** to support OpenBSD and its limitations without burdening
2617
- ** Linux and MacOS with lots of extra code and complication. */
2618
- zIpAddr = "127.0.0.1";
2619
- }
2620
- if( zIpAddr ){
2621
- if( strchr(zIpAddr,':') ){
2622
- memset(&inaddr, 0, sizeof(inaddr));
2623
- inaddr.sin6_family = AF_INET6;
2624
- bIPv4 = 0;
2625
- if( inet_pton(AF_INET6, zIpAddr, &inaddr.sin6_addr)==0 ){
2626
- fossil_fatal("not a valid IPv6 address: %s", zIpAddr);
2627
- }
2628
- }else{
2629
- memset(&inaddr4, 0, sizeof(inaddr4));
2630
- inaddr4.sin_family = AF_INET;
2631
- bIPv4 = 1;
2632
- inaddr4.sin_addr.s_addr = inet_addr(zIpAddr);
2633
- if( inaddr4.sin_addr.s_addr == INADDR_NONE ){
2634
- fossil_fatal("not a valid IPv4 address: %s", zIpAddr);
2635
- }
2636
- }
2637
- }else{
2638
- /* Bind to any and all available IP addresses */
2639
- memset(&inaddr, 0, sizeof(inaddr));
2640
- inaddr.sin6_family = AF_INET6;
2641
- inaddr.sin6_addr = in6addr_any;
2642
- bIPv4 = 0;
2643
- }
2644
- if( bIPv4 ){
2645
- inaddr4.sin_port = htons(iPort);
2646
- listener = socket(AF_INET, SOCK_STREAM, 0);
2647
- }else{
2648
- inaddr.sin6_port = htons(iPort);
2649
- listener = socket(AF_INET6, SOCK_STREAM, 0);
2650
- allowBothIpV4andV6(listener);
2651
- }
2652
- if( listener<0 ){
2653
- iPort++;
2654
- continue;
2655
- }
2656
- }
2657
-
2658
- /* if we can't terminate nicely, at least allow the socket to be reused */
2659
- setsockopt(listener,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
2660
-
2661
- if( flags & HTTP_SERVER_UNIXSOCKET ){
2662
- rc = bind(listener, (struct sockaddr*)&uxaddr, sizeof(uxaddr));
2663
- /* Set the owner of the socket if requested by --socket-owner. This
2664
- ** must wait until after bind(), after the filesystem object has been
2665
- ** created. See https://lkml.org/lkml/2004/11/1/84 and
2666
- ** https://fossil-scm.org/forum/forumpost/7517680ef9684c57 */
2667
- if( g.zSockOwner ){
2668
- file_set_owner(g.zSockName, listener, g.zSockOwner);
2669
- }
2670
- }else if( bIPv4 ){
2671
- rc = bind(listener, (struct sockaddr*)&inaddr4, sizeof(inaddr4));
2672
- }else{
2673
- rc = bind(listener, (struct sockaddr*)&inaddr, sizeof(inaddr));
2674
- }
2675
- if( rc<0 ){
2676
- close(listener);
2677
- iPort++;
2678
- continue;
2679
- }
2680
- break;
2681
- }
2682
- if( iPort>mxPort ){
2683
- if( flags & HTTP_SERVER_UNIXSOCKET ){
2684
- fossil_fatal("unable to listen on unix socket %s", zIpAddr);
2685
- }else if( mnPort==mxPort ){
2686
- fossil_fatal("unable to open listening socket on port %d", mnPort);
2687
- }else{
2688
- fossil_fatal("unable to open listening socket on any"
2689
- " port in the range %d..%d", mnPort, mxPort);
2690
- }
2691
- }
2692
- if( iPort>mxPort ) return 1;
2693
- listen(listener,10);
2694
- if( flags & HTTP_SERVER_UNIXSOCKET ){
2695
- fossil_print("Listening for %s requests on unix socket %s\n",
2696
- (flags & HTTP_SERVER_SCGI)!=0 ? "SCGI" :
2697
- g.httpUseSSL?"TLS-encrypted HTTPS":"HTTP", g.zSockName);
2698
- }else{
2699
- fossil_print("Listening for %s requests on TCP port %d\n",
2700
- (flags & HTTP_SERVER_SCGI)!=0 ? "SCGI" :
2701
- g.httpUseSSL?"TLS-encrypted HTTPS":"HTTP", iPort);
2702
- }
2703
- fflush(stdout);
2561
+ const char *zRequestType; /* Type of requests to listen for */
2562
+
2563
+
2564
+ if( flags & HTTP_SERVER_SCGI ){
2565
+ zRequestType = "SCGI";
2566
+ }else if( g.httpUseSSL ){
2567
+ zRequestType = "TLS-encrypted HTTPS";
2568
+ }else{
2569
+ zRequestType = "HTTP";
2570
+ }
2571
+
2572
+ if( flags & HTTP_SERVER_UNIXSOCKET ){
2573
+ /* CASE 1: A unix socket named g.zSockName. After creation, set the
2574
+ ** permissions on the new socket to g.zSockMode and make the
2575
+ ** owner of the socket be g.zSockOwner.
2576
+ */
2577
+ assert( g.zSockName!=0 );
2578
+ memset(&uxaddr, 0, sizeof(uxaddr));
2579
+ if( strlen(g.zSockName)>sizeof(uxaddr.sun_path) ){
2580
+ fossil_fatal("name of unix socket too big: %s\nmax size: %d\n",
2581
+ g.zSockName, (int)sizeof(uxaddr.sun_path));
2582
+ }
2583
+ if( file_isdir(g.zSockName, ExtFILE)!=0 ){
2584
+ if( !file_issocket(g.zSockName) ){
2585
+ fossil_fatal("cannot name socket \"%s\" because another object"
2586
+ " with that name already exists", g.zSockName);
2587
+ }else{
2588
+ unlink(g.zSockName);
2589
+ }
2590
+ }
2591
+ uxaddr.sun_family = AF_UNIX;
2592
+ strncpy(uxaddr.sun_path, g.zSockName, sizeof(uxaddr.sun_path)-1);
2593
+ listen4 = socket(AF_UNIX, SOCK_STREAM, 0);
2594
+ if( listen4<0 ){
2595
+ fossil_fatal("unable to create a unix socket named %s",
2596
+ g.zSockName);
2597
+ }
2598
+ mxListen = listen4;
2599
+ listen6 = -1;
2600
+
2601
+ /* Set the access permission for the new socket. Default to 0660.
2602
+ ** But use an alternative specified by --socket-mode if available.
2603
+ ** Do this before bind() to avoid a race condition. */
2604
+ if( g.zSockMode ){
2605
+ file_set_mode(g.zSockName, listen4, g.zSockMode, 0);
2606
+ }else{
2607
+ file_set_mode(g.zSockName, listen4, "0660", 1);
2608
+ }
2609
+ rc = bind(listen4, (struct sockaddr*)&uxaddr, sizeof(uxaddr));
2610
+ /* Set the owner of the socket if requested by --socket-owner. This
2611
+ ** must wait until after bind(), after the filesystem object has been
2612
+ ** created. See https://lkml.org/lkml/2004/11/1/84 and
2613
+ ** https://fossil-scm.org/forum/forumpost/7517680ef9684c57 */
2614
+ if( g.zSockOwner ){
2615
+ file_set_owner(g.zSockName, listen4, g.zSockOwner);
2616
+ }
2617
+ fossil_print("Listening for %s requests on unix socket %s\n",
2618
+ zRequestType, g.zSockName);
2619
+ fflush(stdout);
2620
+ }else if( zIpAddr && strchr(zIpAddr,':')!=0 ){
2621
+ /* CASE 2: TCP on IPv6 IP address specified by zIpAddr and on port iPort.
2622
+ */
2623
+ assert( mnPort==mxPort );
2624
+ memset(&inaddr6, 0, sizeof(inaddr6));
2625
+ inaddr6.sin6_family = AF_INET6;
2626
+ inaddr6.sin6_port = htons(iPort);
2627
+ if( inet_pton(AF_INET6, zIpAddr, &inaddr6.sin6_addr)==0 ){
2628
+ fossil_fatal("not a valid IPv6 address: %s", zIpAddr);
2629
+ }
2630
+ listen6 = socket(AF_INET6, SOCK_STREAM, 0);
2631
+ if( listen6>0 ){
2632
+ opt = 1;
2633
+ setsockopt(listen6, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
2634
+ rc = bind(listen6, (struct sockaddr*)&inaddr6, sizeof(inaddr6));
2635
+ if( rc<0 ){
2636
+ close(listen6);
2637
+ listen6 = -1;
2638
+ }
2639
+ }
2640
+ if( listen6<0 ){
2641
+ fossil_fatal("cannot open a listening socket on [%s]:%d",
2642
+ zIpAddr, mnPort);
2643
+ }
2644
+ mxListen = listen6;
2645
+ listen4 = -1;
2646
+ fossil_print("Listening for %s requests on [%s]:%d\n",
2647
+ zRequestType, zIpAddr, iPort);
2648
+ fflush(stdout);
2649
+ }else if( zIpAddr && zIpAddr[0] ){
2650
+ /* CASE 3: TCP on IPv4 IP address specified by zIpAddr and on port iPort.
2651
+ */
2652
+ assert( mnPort==mxPort );
2653
+ memset(&inaddr4, 0, sizeof(inaddr4));
2654
+ inaddr4.sin_family = AF_INET;
2655
+ inaddr4.sin_port = htons(iPort);
2656
+ if( strcmp(zIpAddr, "localhost")==0 ) zIpAddr = "127.0.0.1";
2657
+ inaddr4.sin_addr.s_addr = inet_addr(zIpAddr);
2658
+ if( inaddr4.sin_addr.s_addr == INADDR_NONE ){
2659
+ fossil_fatal("not a valid IPv4 address: %s", zIpAddr);
2660
+ }
2661
+ listen4 = socket(AF_INET, SOCK_STREAM, 0);
2662
+ if( listen4>0 ){
2663
+ setsockopt(listen4, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
2664
+ rc = bind(listen4, (struct sockaddr*)&inaddr4, sizeof(inaddr4));
2665
+ if( rc<0 ){
2666
+ close(listen6);
2667
+ listen4 = -1;
2668
+ }
2669
+ }
2670
+ if( listen4<0 ){
2671
+ fossil_fatal("cannot open a listening socket on %s:%d",
2672
+ zIpAddr, mnPort);
2673
+ }
2674
+ mxListen = listen4;
2675
+ listen6 = -1;
2676
+ fossil_print("Listening for %s requests on TCP port %s:%d\n",
2677
+ zRequestType, zIpAddr, iPort);
2678
+ fflush(stdout);
2679
+ }else{
2680
+ /* CASE 4: Listen on all available IP addresses, or on only loopback
2681
+ ** addresses (if HTTP_SERVER_LOCALHOST). The TCP port is the
2682
+ ** first available in the range of mnPort..mxPort. Listen
2683
+ ** on both IPv4 and IPv6, if possible. The TCP port scan is done
2684
+ ** on IPv4.
2685
+ */
2686
+ while( iPort<=mxPort ){
2687
+ const char *zProto;
2688
+ memset(&inaddr4, 0, sizeof(inaddr4));
2689
+ inaddr4.sin_family = AF_INET;
2690
+ inaddr4.sin_port = htons(iPort);
2691
+ if( flags & HTTP_SERVER_LOCALHOST ){
2692
+ inaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
2693
+ }else{
2694
+ inaddr4.sin_addr.s_addr = htonl(INADDR_ANY);
2695
+ }
2696
+ listen4 = socket(AF_INET, SOCK_STREAM, 0);
2697
+ if( listen4>0 ){
2698
+ setsockopt(listen4, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
2699
+ rc = bind(listen4, (struct sockaddr*)&inaddr4, sizeof(inaddr4));
2700
+ if( rc<0 ){
2701
+ close(listen4);
2702
+ listen4 = -1;
2703
+ }
2704
+ }
2705
+ if( listen4<0 ){
2706
+ iPort++;
2707
+ continue;
2708
+ }
2709
+ mxListen = listen4;
2710
+
2711
+ /* If we get here, that means we found an open TCP port at iPort for
2712
+ ** IPv4. Try to set up a corresponding IPv6 socket on the same port.
2713
+ */
2714
+ memset(&inaddr6, 0, sizeof(inaddr6));
2715
+ inaddr6.sin6_family = AF_INET6;
2716
+ inaddr6.sin6_port = htons(iPort);
2717
+ if( flags & HTTP_SERVER_LOCALHOST ){
2718
+ inaddr6.sin6_addr = in6addr_loopback;
2719
+ }else{
2720
+ inaddr6.sin6_addr = in6addr_any;
2721
+ }
2722
+ listen6 = socket(AF_INET6, SOCK_STREAM, 0);
2723
+ if( listen6>0 ){
2724
+ setsockopt(listen6, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
2725
+ setsockopt(listen6, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt));
2726
+ rc = bind(listen6, (struct sockaddr*)&inaddr6, sizeof(inaddr6));
2727
+ if( rc<0 ){
2728
+ close(listen6);
2729
+ listen6 = -1;
2730
+ }
2731
+ }
2732
+ if( listen6<0 ){
2733
+ zProto = "IPv4 only";
2734
+ }else{
2735
+ zProto = "IPv4 and IPv6";
2736
+ if( listen6>listen4 ) mxListen = listen6;
2737
+ }
2738
+
2739
+ fossil_print("Listening for %s requests on TCP port %s%d, %s\n",
2740
+ zRequestType,
2741
+ (flags & HTTP_SERVER_LOCALHOST)!=0 ? "localhost:" : "",
2742
+ iPort, zProto);
2743
+ fflush(stdout);
2744
+ break;
2745
+ }
2746
+ if( iPort>mxPort ){
2747
+ fossil_fatal("no available TCP ports in the range %d..%d",
2748
+ mnPort, mxPort);
2749
+ }
2750
+ }
2751
+
2752
+ /* If we get to this point, that means there is at least one listening
2753
+ ** socket on either listen4 or listen6 and perhaps on both. */
2754
+ assert( listen4>0 || listen6>0 );
2755
+ if( listen4>0 ) listen(listen4,10);
2756
+ if( listen6>0 ) listen(listen6,10);
27042757
if( zBrowser && (flags & HTTP_SERVER_UNIXSOCKET)==0 ){
27052758
assert( strstr(zBrowser,"%d")!=0 );
27062759
zBrowser = mprintf(zBrowser /*works-like:"%d"*/, iPort);
27072760
#if defined(__CYGWIN__)
27082761
/* On Cygwin, we can do better than "echo" */
@@ -2716,57 +2769,69 @@
27162769
#endif
27172770
if( fossil_system(zBrowser)<0 ){
27182771
fossil_warning("cannot start browser: %s\n", zBrowser);
27192772
}
27202773
}
2774
+
2775
+ /* What for incomming requests. For each request, fork() a child process
2776
+ ** to deal with that request. The child process returns. The parent
2777
+ ** keeps on listening and never returns.
2778
+ */
27212779
while( 1 ){
27222780
#if FOSSIL_MAX_CONNECTIONS>0
27232781
while( nchildren>=FOSSIL_MAX_CONNECTIONS ){
27242782
if( wait(0)>=0 ) nchildren--;
27252783
}
27262784
#endif
27272785
delay.tv_sec = 0;
27282786
delay.tv_usec = 100000;
27292787
FD_ZERO(&readfds);
2730
- assert( listener>=0 );
2731
- FD_SET( listener, &readfds);
2732
- select( listener+1, &readfds, 0, 0, &delay);
2733
- if( FD_ISSET(listener, &readfds) ){
2734
- lenaddr = sizeof(inaddr);
2735
- connection = accept(listener, (struct sockaddr*)&inaddr, &lenaddr);
2736
- if( connection>=0 ){
2737
- if( flags & HTTP_SERVER_NOFORK ){
2738
- child = 0;
2739
- }else{
2740
- child = fork();
2741
- }
2742
- if( child!=0 ){
2743
- if( child>0 ){
2744
- nchildren++;
2745
- nRequest++;
2746
- }
2747
- close(connection);
2748
- }else{
2749
- int nErr = 0, fd;
2750
- g.zSockName = 0 /* avoid deleting the socket via atexit() */;
2751
- close(0);
2752
- fd = dup(connection);
2753
- if( fd!=0 ) nErr++;
2754
- close(1);
2755
- fd = dup(connection);
2756
- if( fd!=1 ) nErr++;
2757
- if( 0 && !g.fAnyTrace ){
2758
- close(2);
2759
- fd = dup(connection);
2760
- if( fd!=2 ) nErr++;
2761
- }
2762
- close(connection);
2763
- close(listener);
2764
- g.nPendingRequest = nchildren+1;
2765
- g.nRequest = nRequest+1;
2766
- return nErr;
2767
- }
2788
+ assert( listen4>0 || listen6>0 );
2789
+ if( listen4>0 ) FD_SET( listen4, &readfds);
2790
+ if( listen6>0 ) FD_SET( listen6, &readfds);
2791
+ select( mxListen+1, &readfds, 0, 0, &delay);
2792
+ if( listen4>0 && FD_ISSET(listen4, &readfds) ){
2793
+ lenaddr = sizeof(inaddr4);
2794
+ connection = accept(listen4, (struct sockaddr*)&inaddr4, &lenaddr);
2795
+ }else if( listen6>0 && FD_ISSET(listen6, &readfds) ){
2796
+ lenaddr = sizeof(inaddr6);
2797
+ connection = accept(listen6, (struct sockaddr*)&inaddr6, &lenaddr);
2798
+ }else{
2799
+ connection = -1;
2800
+ }
2801
+ if( connection>=0 ){
2802
+ if( flags & HTTP_SERVER_NOFORK ){
2803
+ child = 0;
2804
+ }else{
2805
+ child = fork();
2806
+ }
2807
+ if( child!=0 ){
2808
+ if( child>0 ){
2809
+ nchildren++;
2810
+ nRequest++;
2811
+ }
2812
+ close(connection);
2813
+ }else{
2814
+ int nErr = 0, fd;
2815
+ g.zSockName = 0 /* avoid deleting the socket via atexit() */;
2816
+ close(0);
2817
+ fd = dup(connection);
2818
+ if( fd!=0 ) nErr++;
2819
+ close(1);
2820
+ fd = dup(connection);
2821
+ if( fd!=1 ) nErr++;
2822
+ if( 0 && !g.fAnyTrace ){
2823
+ close(2);
2824
+ fd = dup(connection);
2825
+ if( fd!=2 ) nErr++;
2826
+ }
2827
+ close(connection);
2828
+ if( listen4>0 ) close(listen4);
2829
+ if( listen6>0 ) close(listen6);
2830
+ g.nPendingRequest = nchildren+1;
2831
+ g.nRequest = nRequest+1;
2832
+ return nErr;
27682833
}
27692834
}
27702835
/* Bury dead children */
27712836
if( nchildren ){
27722837
while(1){
27732838
--- src/cgi.c
+++ src/cgi.c
@@ -2497,29 +2497,10 @@
2497 }
2498 fossil_free(zToFree);
2499 fgetc(g.httpIn); /* Read past the "," separating header from content */
2500 cgi_init();
2501 }
2502
2503 #if !defined(_WIN32)
2504 /*
2505 ** Change the listening socket, if necessary, so that it will accept both IPv4
2506 ** and IPv6
2507 */
2508 static void allowBothIpV4andV6(int listener){
2509 #if defined(IPV6_V6ONLY)
2510 int ipv6only = -1;
2511 socklen_t ipv6only_size = sizeof(ipv6only);
2512 getsockopt(listener, IPPROTO_IPV6, IPV6_V6ONLY, &ipv6only, &ipv6only_size);
2513 if( ipv6only ){
2514 ipv6only = 0;
2515 setsockopt(listener, IPPROTO_IPV6, IPV6_V6ONLY, &ipv6only, ipv6only_size);
2516 }
2517 #endif /* defined(IPV6_ONLY) */
2518 }
2519 #endif /* !defined(_WIN32) */
2520
2521
2522 #if INTERFACE
2523 /*
2524 ** Bitmap values for the flags parameter to cgi_http_server().
2525 */
@@ -2559,150 +2540,222 @@
2559 ){
2560 #if defined(_WIN32)
2561 /* Use win32_http_server() instead */
2562 fossil_exit(1);
2563 #else
2564 int listener = -1; /* The server socket */
2565 int connection; /* A socket for each individual connection */
 
 
2566 int nRequest = 0; /* Number of requests handled so far */
2567 fd_set readfds; /* Set of file descriptors for select() */
2568 socklen_t lenaddr; /* Length of the inaddr structure */
2569 int child; /* PID of the child process */
2570 int nchildren = 0; /* Number of child processes */
2571 struct timeval delay; /* How long to wait inside select() */
2572 struct sockaddr_in6 inaddr; /* The socket address */
2573 struct sockaddr_in inaddr4; /* IPv4 address; needed by OpenBSD */
2574 struct sockaddr_un uxaddr; /* The address for unix-domain sockets */
2575 int opt = 1; /* setsockopt flag */
2576 int rc; /* Result code from system calls */
2577 int iPort = mnPort; /* Port to try to use */
2578 int bIPv4 = 0; /* Use IPv4 only; use inaddr4, not inaddr */
2579
2580 while( iPort<=mxPort ){
2581 if( flags & HTTP_SERVER_UNIXSOCKET ){
2582 /* Initialize a Unix socket named g.zSockName */
2583 assert( g.zSockName!=0 );
2584 memset(&uxaddr, 0, sizeof(uxaddr));
2585 if( strlen(g.zSockName)>sizeof(uxaddr.sun_path) ){
2586 fossil_fatal("name of unix socket too big: %s\nmax size: %d\n",
2587 g.zSockName, (int)sizeof(uxaddr.sun_path));
2588 }
2589 if( file_isdir(g.zSockName, ExtFILE)!=0 ){
2590 if( !file_issocket(g.zSockName) ){
2591 fossil_fatal("cannot name socket \"%s\" because another object"
2592 " with that name already exists", g.zSockName);
2593 }else{
2594 unlink(g.zSockName);
2595 }
2596 }
2597 uxaddr.sun_family = AF_UNIX;
2598 strncpy(uxaddr.sun_path, g.zSockName, sizeof(uxaddr.sun_path)-1);
2599 listener = socket(AF_UNIX, SOCK_STREAM, 0);
2600 if( listener<0 ){
2601 fossil_fatal("unable to create a unix socket named %s",
2602 g.zSockName);
2603 }
2604 /* Set the access permission for the new socket. Default to 0660.
2605 ** But use an alternative specified by --socket-mode if available.
2606 ** Do this before bind() to avoid a race condition. */
2607 if( g.zSockMode ){
2608 file_set_mode(g.zSockName, listener, g.zSockMode, 0);
2609 }else{
2610 file_set_mode(g.zSockName, listener, "0660", 1);
2611 }
2612 }else{
2613 /* Initialize a TCP/IP socket on port iPort */
2614 if( (flags & HTTP_SERVER_LOCALHOST)!=0 && zIpAddr==0 ){
2615 /* Map all loopback to 127.0.0.1, since this is the easiest way
2616 ** to support OpenBSD and its limitations without burdening
2617 ** Linux and MacOS with lots of extra code and complication. */
2618 zIpAddr = "127.0.0.1";
2619 }
2620 if( zIpAddr ){
2621 if( strchr(zIpAddr,':') ){
2622 memset(&inaddr, 0, sizeof(inaddr));
2623 inaddr.sin6_family = AF_INET6;
2624 bIPv4 = 0;
2625 if( inet_pton(AF_INET6, zIpAddr, &inaddr.sin6_addr)==0 ){
2626 fossil_fatal("not a valid IPv6 address: %s", zIpAddr);
2627 }
2628 }else{
2629 memset(&inaddr4, 0, sizeof(inaddr4));
2630 inaddr4.sin_family = AF_INET;
2631 bIPv4 = 1;
2632 inaddr4.sin_addr.s_addr = inet_addr(zIpAddr);
2633 if( inaddr4.sin_addr.s_addr == INADDR_NONE ){
2634 fossil_fatal("not a valid IPv4 address: %s", zIpAddr);
2635 }
2636 }
2637 }else{
2638 /* Bind to any and all available IP addresses */
2639 memset(&inaddr, 0, sizeof(inaddr));
2640 inaddr.sin6_family = AF_INET6;
2641 inaddr.sin6_addr = in6addr_any;
2642 bIPv4 = 0;
2643 }
2644 if( bIPv4 ){
2645 inaddr4.sin_port = htons(iPort);
2646 listener = socket(AF_INET, SOCK_STREAM, 0);
2647 }else{
2648 inaddr.sin6_port = htons(iPort);
2649 listener = socket(AF_INET6, SOCK_STREAM, 0);
2650 allowBothIpV4andV6(listener);
2651 }
2652 if( listener<0 ){
2653 iPort++;
2654 continue;
2655 }
2656 }
2657
2658 /* if we can't terminate nicely, at least allow the socket to be reused */
2659 setsockopt(listener,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
2660
2661 if( flags & HTTP_SERVER_UNIXSOCKET ){
2662 rc = bind(listener, (struct sockaddr*)&uxaddr, sizeof(uxaddr));
2663 /* Set the owner of the socket if requested by --socket-owner. This
2664 ** must wait until after bind(), after the filesystem object has been
2665 ** created. See https://lkml.org/lkml/2004/11/1/84 and
2666 ** https://fossil-scm.org/forum/forumpost/7517680ef9684c57 */
2667 if( g.zSockOwner ){
2668 file_set_owner(g.zSockName, listener, g.zSockOwner);
2669 }
2670 }else if( bIPv4 ){
2671 rc = bind(listener, (struct sockaddr*)&inaddr4, sizeof(inaddr4));
2672 }else{
2673 rc = bind(listener, (struct sockaddr*)&inaddr, sizeof(inaddr));
2674 }
2675 if( rc<0 ){
2676 close(listener);
2677 iPort++;
2678 continue;
2679 }
2680 break;
2681 }
2682 if( iPort>mxPort ){
2683 if( flags & HTTP_SERVER_UNIXSOCKET ){
2684 fossil_fatal("unable to listen on unix socket %s", zIpAddr);
2685 }else if( mnPort==mxPort ){
2686 fossil_fatal("unable to open listening socket on port %d", mnPort);
2687 }else{
2688 fossil_fatal("unable to open listening socket on any"
2689 " port in the range %d..%d", mnPort, mxPort);
2690 }
2691 }
2692 if( iPort>mxPort ) return 1;
2693 listen(listener,10);
2694 if( flags & HTTP_SERVER_UNIXSOCKET ){
2695 fossil_print("Listening for %s requests on unix socket %s\n",
2696 (flags & HTTP_SERVER_SCGI)!=0 ? "SCGI" :
2697 g.httpUseSSL?"TLS-encrypted HTTPS":"HTTP", g.zSockName);
2698 }else{
2699 fossil_print("Listening for %s requests on TCP port %d\n",
2700 (flags & HTTP_SERVER_SCGI)!=0 ? "SCGI" :
2701 g.httpUseSSL?"TLS-encrypted HTTPS":"HTTP", iPort);
2702 }
2703 fflush(stdout);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2704 if( zBrowser && (flags & HTTP_SERVER_UNIXSOCKET)==0 ){
2705 assert( strstr(zBrowser,"%d")!=0 );
2706 zBrowser = mprintf(zBrowser /*works-like:"%d"*/, iPort);
2707 #if defined(__CYGWIN__)
2708 /* On Cygwin, we can do better than "echo" */
@@ -2716,57 +2769,69 @@
2716 #endif
2717 if( fossil_system(zBrowser)<0 ){
2718 fossil_warning("cannot start browser: %s\n", zBrowser);
2719 }
2720 }
 
 
 
 
 
2721 while( 1 ){
2722 #if FOSSIL_MAX_CONNECTIONS>0
2723 while( nchildren>=FOSSIL_MAX_CONNECTIONS ){
2724 if( wait(0)>=0 ) nchildren--;
2725 }
2726 #endif
2727 delay.tv_sec = 0;
2728 delay.tv_usec = 100000;
2729 FD_ZERO(&readfds);
2730 assert( listener>=0 );
2731 FD_SET( listener, &readfds);
2732 select( listener+1, &readfds, 0, 0, &delay);
2733 if( FD_ISSET(listener, &readfds) ){
2734 lenaddr = sizeof(inaddr);
2735 connection = accept(listener, (struct sockaddr*)&inaddr, &lenaddr);
2736 if( connection>=0 ){
2737 if( flags & HTTP_SERVER_NOFORK ){
2738 child = 0;
2739 }else{
2740 child = fork();
2741 }
2742 if( child!=0 ){
2743 if( child>0 ){
2744 nchildren++;
2745 nRequest++;
2746 }
2747 close(connection);
2748 }else{
2749 int nErr = 0, fd;
2750 g.zSockName = 0 /* avoid deleting the socket via atexit() */;
2751 close(0);
2752 fd = dup(connection);
2753 if( fd!=0 ) nErr++;
2754 close(1);
2755 fd = dup(connection);
2756 if( fd!=1 ) nErr++;
2757 if( 0 && !g.fAnyTrace ){
2758 close(2);
2759 fd = dup(connection);
2760 if( fd!=2 ) nErr++;
2761 }
2762 close(connection);
2763 close(listener);
2764 g.nPendingRequest = nchildren+1;
2765 g.nRequest = nRequest+1;
2766 return nErr;
2767 }
 
 
 
 
 
 
 
2768 }
2769 }
2770 /* Bury dead children */
2771 if( nchildren ){
2772 while(1){
2773
--- src/cgi.c
+++ src/cgi.c
@@ -2497,29 +2497,10 @@
2497 }
2498 fossil_free(zToFree);
2499 fgetc(g.httpIn); /* Read past the "," separating header from content */
2500 cgi_init();
2501 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2502
2503 #if INTERFACE
2504 /*
2505 ** Bitmap values for the flags parameter to cgi_http_server().
2506 */
@@ -2559,150 +2540,222 @@
2540 ){
2541 #if defined(_WIN32)
2542 /* Use win32_http_server() instead */
2543 fossil_exit(1);
2544 #else
2545 int listen4 = -1; /* Main socket; IPv4 or unix-domain */
2546 int listen6 = -1; /* Aux socket for corresponding IPv6 */
2547 int mxListen = -1; /* Maximum of listen4 and listen6 */
2548 int connection; /* An incoming connection */
2549 int nRequest = 0; /* Number of requests handled so far */
2550 fd_set readfds; /* Set of file descriptors for select() */
2551 socklen_t lenaddr; /* Length of the inaddr structure */
2552 int child; /* PID of the child process */
2553 int nchildren = 0; /* Number of child processes */
2554 struct timeval delay; /* How long to wait inside select() */
2555 struct sockaddr_in6 inaddr6; /* Address for IPv6 */
2556 struct sockaddr_in inaddr4; /* Address for IPv4 */
2557 struct sockaddr_un uxaddr; /* The address for unix-domain sockets */
2558 int opt = 1; /* setsockopt flag */
2559 int rc; /* Result code from system calls */
2560 int iPort = mnPort; /* Port to try to use */
2561 const char *zRequestType; /* Type of requests to listen for */
2562
2563
2564 if( flags & HTTP_SERVER_SCGI ){
2565 zRequestType = "SCGI";
2566 }else if( g.httpUseSSL ){
2567 zRequestType = "TLS-encrypted HTTPS";
2568 }else{
2569 zRequestType = "HTTP";
2570 }
2571
2572 if( flags & HTTP_SERVER_UNIXSOCKET ){
2573 /* CASE 1: A unix socket named g.zSockName. After creation, set the
2574 ** permissions on the new socket to g.zSockMode and make the
2575 ** owner of the socket be g.zSockOwner.
2576 */
2577 assert( g.zSockName!=0 );
2578 memset(&uxaddr, 0, sizeof(uxaddr));
2579 if( strlen(g.zSockName)>sizeof(uxaddr.sun_path) ){
2580 fossil_fatal("name of unix socket too big: %s\nmax size: %d\n",
2581 g.zSockName, (int)sizeof(uxaddr.sun_path));
2582 }
2583 if( file_isdir(g.zSockName, ExtFILE)!=0 ){
2584 if( !file_issocket(g.zSockName) ){
2585 fossil_fatal("cannot name socket \"%s\" because another object"
2586 " with that name already exists", g.zSockName);
2587 }else{
2588 unlink(g.zSockName);
2589 }
2590 }
2591 uxaddr.sun_family = AF_UNIX;
2592 strncpy(uxaddr.sun_path, g.zSockName, sizeof(uxaddr.sun_path)-1);
2593 listen4 = socket(AF_UNIX, SOCK_STREAM, 0);
2594 if( listen4<0 ){
2595 fossil_fatal("unable to create a unix socket named %s",
2596 g.zSockName);
2597 }
2598 mxListen = listen4;
2599 listen6 = -1;
2600
2601 /* Set the access permission for the new socket. Default to 0660.
2602 ** But use an alternative specified by --socket-mode if available.
2603 ** Do this before bind() to avoid a race condition. */
2604 if( g.zSockMode ){
2605 file_set_mode(g.zSockName, listen4, g.zSockMode, 0);
2606 }else{
2607 file_set_mode(g.zSockName, listen4, "0660", 1);
2608 }
2609 rc = bind(listen4, (struct sockaddr*)&uxaddr, sizeof(uxaddr));
2610 /* Set the owner of the socket if requested by --socket-owner. This
2611 ** must wait until after bind(), after the filesystem object has been
2612 ** created. See https://lkml.org/lkml/2004/11/1/84 and
2613 ** https://fossil-scm.org/forum/forumpost/7517680ef9684c57 */
2614 if( g.zSockOwner ){
2615 file_set_owner(g.zSockName, listen4, g.zSockOwner);
2616 }
2617 fossil_print("Listening for %s requests on unix socket %s\n",
2618 zRequestType, g.zSockName);
2619 fflush(stdout);
2620 }else if( zIpAddr && strchr(zIpAddr,':')!=0 ){
2621 /* CASE 2: TCP on IPv6 IP address specified by zIpAddr and on port iPort.
2622 */
2623 assert( mnPort==mxPort );
2624 memset(&inaddr6, 0, sizeof(inaddr6));
2625 inaddr6.sin6_family = AF_INET6;
2626 inaddr6.sin6_port = htons(iPort);
2627 if( inet_pton(AF_INET6, zIpAddr, &inaddr6.sin6_addr)==0 ){
2628 fossil_fatal("not a valid IPv6 address: %s", zIpAddr);
2629 }
2630 listen6 = socket(AF_INET6, SOCK_STREAM, 0);
2631 if( listen6>0 ){
2632 opt = 1;
2633 setsockopt(listen6, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
2634 rc = bind(listen6, (struct sockaddr*)&inaddr6, sizeof(inaddr6));
2635 if( rc<0 ){
2636 close(listen6);
2637 listen6 = -1;
2638 }
2639 }
2640 if( listen6<0 ){
2641 fossil_fatal("cannot open a listening socket on [%s]:%d",
2642 zIpAddr, mnPort);
2643 }
2644 mxListen = listen6;
2645 listen4 = -1;
2646 fossil_print("Listening for %s requests on [%s]:%d\n",
2647 zRequestType, zIpAddr, iPort);
2648 fflush(stdout);
2649 }else if( zIpAddr && zIpAddr[0] ){
2650 /* CASE 3: TCP on IPv4 IP address specified by zIpAddr and on port iPort.
2651 */
2652 assert( mnPort==mxPort );
2653 memset(&inaddr4, 0, sizeof(inaddr4));
2654 inaddr4.sin_family = AF_INET;
2655 inaddr4.sin_port = htons(iPort);
2656 if( strcmp(zIpAddr, "localhost")==0 ) zIpAddr = "127.0.0.1";
2657 inaddr4.sin_addr.s_addr = inet_addr(zIpAddr);
2658 if( inaddr4.sin_addr.s_addr == INADDR_NONE ){
2659 fossil_fatal("not a valid IPv4 address: %s", zIpAddr);
2660 }
2661 listen4 = socket(AF_INET, SOCK_STREAM, 0);
2662 if( listen4>0 ){
2663 setsockopt(listen4, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
2664 rc = bind(listen4, (struct sockaddr*)&inaddr4, sizeof(inaddr4));
2665 if( rc<0 ){
2666 close(listen6);
2667 listen4 = -1;
2668 }
2669 }
2670 if( listen4<0 ){
2671 fossil_fatal("cannot open a listening socket on %s:%d",
2672 zIpAddr, mnPort);
2673 }
2674 mxListen = listen4;
2675 listen6 = -1;
2676 fossil_print("Listening for %s requests on TCP port %s:%d\n",
2677 zRequestType, zIpAddr, iPort);
2678 fflush(stdout);
2679 }else{
2680 /* CASE 4: Listen on all available IP addresses, or on only loopback
2681 ** addresses (if HTTP_SERVER_LOCALHOST). The TCP port is the
2682 ** first available in the range of mnPort..mxPort. Listen
2683 ** on both IPv4 and IPv6, if possible. The TCP port scan is done
2684 ** on IPv4.
2685 */
2686 while( iPort<=mxPort ){
2687 const char *zProto;
2688 memset(&inaddr4, 0, sizeof(inaddr4));
2689 inaddr4.sin_family = AF_INET;
2690 inaddr4.sin_port = htons(iPort);
2691 if( flags & HTTP_SERVER_LOCALHOST ){
2692 inaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
2693 }else{
2694 inaddr4.sin_addr.s_addr = htonl(INADDR_ANY);
2695 }
2696 listen4 = socket(AF_INET, SOCK_STREAM, 0);
2697 if( listen4>0 ){
2698 setsockopt(listen4, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
2699 rc = bind(listen4, (struct sockaddr*)&inaddr4, sizeof(inaddr4));
2700 if( rc<0 ){
2701 close(listen4);
2702 listen4 = -1;
2703 }
2704 }
2705 if( listen4<0 ){
2706 iPort++;
2707 continue;
2708 }
2709 mxListen = listen4;
2710
2711 /* If we get here, that means we found an open TCP port at iPort for
2712 ** IPv4. Try to set up a corresponding IPv6 socket on the same port.
2713 */
2714 memset(&inaddr6, 0, sizeof(inaddr6));
2715 inaddr6.sin6_family = AF_INET6;
2716 inaddr6.sin6_port = htons(iPort);
2717 if( flags & HTTP_SERVER_LOCALHOST ){
2718 inaddr6.sin6_addr = in6addr_loopback;
2719 }else{
2720 inaddr6.sin6_addr = in6addr_any;
2721 }
2722 listen6 = socket(AF_INET6, SOCK_STREAM, 0);
2723 if( listen6>0 ){
2724 setsockopt(listen6, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
2725 setsockopt(listen6, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt));
2726 rc = bind(listen6, (struct sockaddr*)&inaddr6, sizeof(inaddr6));
2727 if( rc<0 ){
2728 close(listen6);
2729 listen6 = -1;
2730 }
2731 }
2732 if( listen6<0 ){
2733 zProto = "IPv4 only";
2734 }else{
2735 zProto = "IPv4 and IPv6";
2736 if( listen6>listen4 ) mxListen = listen6;
2737 }
2738
2739 fossil_print("Listening for %s requests on TCP port %s%d, %s\n",
2740 zRequestType,
2741 (flags & HTTP_SERVER_LOCALHOST)!=0 ? "localhost:" : "",
2742 iPort, zProto);
2743 fflush(stdout);
2744 break;
2745 }
2746 if( iPort>mxPort ){
2747 fossil_fatal("no available TCP ports in the range %d..%d",
2748 mnPort, mxPort);
2749 }
2750 }
2751
2752 /* If we get to this point, that means there is at least one listening
2753 ** socket on either listen4 or listen6 and perhaps on both. */
2754 assert( listen4>0 || listen6>0 );
2755 if( listen4>0 ) listen(listen4,10);
2756 if( listen6>0 ) listen(listen6,10);
2757 if( zBrowser && (flags & HTTP_SERVER_UNIXSOCKET)==0 ){
2758 assert( strstr(zBrowser,"%d")!=0 );
2759 zBrowser = mprintf(zBrowser /*works-like:"%d"*/, iPort);
2760 #if defined(__CYGWIN__)
2761 /* On Cygwin, we can do better than "echo" */
@@ -2716,57 +2769,69 @@
2769 #endif
2770 if( fossil_system(zBrowser)<0 ){
2771 fossil_warning("cannot start browser: %s\n", zBrowser);
2772 }
2773 }
2774
2775 /* What for incomming requests. For each request, fork() a child process
2776 ** to deal with that request. The child process returns. The parent
2777 ** keeps on listening and never returns.
2778 */
2779 while( 1 ){
2780 #if FOSSIL_MAX_CONNECTIONS>0
2781 while( nchildren>=FOSSIL_MAX_CONNECTIONS ){
2782 if( wait(0)>=0 ) nchildren--;
2783 }
2784 #endif
2785 delay.tv_sec = 0;
2786 delay.tv_usec = 100000;
2787 FD_ZERO(&readfds);
2788 assert( listen4>0 || listen6>0 );
2789 if( listen4>0 ) FD_SET( listen4, &readfds);
2790 if( listen6>0 ) FD_SET( listen6, &readfds);
2791 select( mxListen+1, &readfds, 0, 0, &delay);
2792 if( listen4>0 && FD_ISSET(listen4, &readfds) ){
2793 lenaddr = sizeof(inaddr4);
2794 connection = accept(listen4, (struct sockaddr*)&inaddr4, &lenaddr);
2795 }else if( listen6>0 && FD_ISSET(listen6, &readfds) ){
2796 lenaddr = sizeof(inaddr6);
2797 connection = accept(listen6, (struct sockaddr*)&inaddr6, &lenaddr);
2798 }else{
2799 connection = -1;
2800 }
2801 if( connection>=0 ){
2802 if( flags & HTTP_SERVER_NOFORK ){
2803 child = 0;
2804 }else{
2805 child = fork();
2806 }
2807 if( child!=0 ){
2808 if( child>0 ){
2809 nchildren++;
2810 nRequest++;
2811 }
2812 close(connection);
2813 }else{
2814 int nErr = 0, fd;
2815 g.zSockName = 0 /* avoid deleting the socket via atexit() */;
2816 close(0);
2817 fd = dup(connection);
2818 if( fd!=0 ) nErr++;
2819 close(1);
2820 fd = dup(connection);
2821 if( fd!=1 ) nErr++;
2822 if( 0 && !g.fAnyTrace ){
2823 close(2);
2824 fd = dup(connection);
2825 if( fd!=2 ) nErr++;
2826 }
2827 close(connection);
2828 if( listen4>0 ) close(listen4);
2829 if( listen6>0 ) close(listen6);
2830 g.nPendingRequest = nchildren+1;
2831 g.nRequest = nRequest+1;
2832 return nErr;
2833 }
2834 }
2835 /* Bury dead children */
2836 if( nchildren ){
2837 while(1){
2838

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button