webfakes/0000755000176200001440000000000014740436161012047 5ustar liggesuserswebfakes/tests/0000755000176200001440000000000014740243712013207 5ustar liggesuserswebfakes/tests/testthat/0000755000176200001440000000000014740436161015051 5ustar liggesuserswebfakes/tests/testthat/test-mw-multipart.R0000644000176200001440000000155214172041777020622 0ustar liggesusers app <- new_app() app$use(mw_multipart()) app$put("/form", function(req, res) { ret = list(form = req$form, files = req$files) res$send_json(ret, pretty = TRUE, auto_unbox = TRUE) }) web <- local_app_process(app) test_that("mw_multipart", { on.exit(rm(tmp), add = TRUE) tmp <- tempfile() writeBin(charToRaw("foobar\n"), con = tmp) url <- web$url("/form") handle <- curl::new_handle() curl::handle_setopt(handle, customrequest = "PUT") curl::handle_setform( handle, a = "1", b = "2", c = curl::form_file(tmp, type = "text/plain") ) resp <- curl::curl_fetch_memory(url, handle = handle) echo <- jsonlite::fromJSON(rawToChar(resp$content), simplifyVector = FALSE) expect_equal(echo$form, list(a = "1", b = "2")) expect_equal( echo$files$c, list( filename = basename(tmp), value = base64_encode("foobar\n") ) ) }) webfakes/tests/testthat/test-print.R0000644000176200001440000000373014172041777017314 0ustar liggesusers app <- new_app() app$use(function(req, res) { tmp <- tempfile() saveRDS(list(req = req, res = res), file = tmp) res$ set_status(200)$ send(normalizePath(tmp)) }) proc <- local_app_process(app) withr::local_options(list(HTTPUserAgent = "It is me, libcurl")) resp <- curl::curl_fetch_memory(proc$url()) tmp <- rawToChar(resp$content) withr::defer(unlink(tmp)) # Verify_output uses a png() graphics device, and fails if there is # no png() support. So we skip theses tests then. capabilities() # is very slow on macOS, because it starts up X11, so we'll just assume # that macOS has a png device. skip_without_png_device <- function() { if (.Platform$OS.type == "windows") return() if (! capabilities("png") || ! capabilities("X11")) { skip("Needs a PNG device") } } test_that("webfakes_app", { skip_without_png_device() app <- new_app() app$use("add etag" = mw_etag()) app$get("/api", function(req, res) res$send("foobar")) verify_output( test_path("fixtures", "output", "webfakes_app.txt"), app ) }) test_that("webfakes_request", { skip_without_png_device() req <- readRDS(tmp)$req req$url <- "http://127.0.0.1:3000/" req$headers$Host <- "127.0.0.1:3000" req$headers$`Accept-Encoding` <- "deflate, gzip" verify_output( test_path("fixtures", "output", "webfakes_request.txt"), req ) }) test_that("webfakes_response", { skip_without_png_device() res <- readRDS(tmp)$res verify_output( test_path("fixtures", "output", "webfakes_response.txt"), res ) }) test_that("webfakes_regexp", { skip_without_png_device() verify_output( test_path("fixtures", "output", "webfakes_regexp.txt"), new_regexp("^(foo|bar)$") ) }) test_that("webfakes_app_process", { skip_without_png_device() app <- new_app() proc <- new_app_process(app, start = TRUE) proc$stop() # make the output deterministic proc$.port <- 3000 verify_output( test_path("fixtures", "output", "webfakes_app_process.txt"), proc ) }) webfakes/tests/testthat/test-oauth.R0000644000176200001440000000777014172041777017310 0ustar liggesusers test_that("oauth2", { skip_on_cran() # Create the resource server rsapp <- local_app_process( oauth2_resource_app(), opts = server_opts(num_threads = 3) ) regi_url <- rsapp$url("/register") auth_url <- rsapp$url("/authorize") toke_url <- rsapp$url("/token") # Create the third party app server tpapp <- local_app_process( oauth2_third_party_app("3P app"), opts = server_opts(num_threads = 3) ) redi_url <- tpapp$url("/login/redirect") conf_url <- tpapp$url("/login/config") # Register the third party app at the resource server # In real life this is done by the admin of the third party app url <- paste0( regi_url, "?name=3P%20app", "&redirect_uri=", redi_url ) resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 200L) regdata <- jsonlite::fromJSON(rawToChar(resp$content)) # Now set this data on the third party app # In real life this is included in the config of the third party app # by its admin auth_data <- jsonlite::toJSON(list( auth_url = auth_url, token_url = toke_url, client_id = regdata$client_id, client_secret = regdata$client_secret ), auto_unbox = TRUE) handle <- curl::new_handle() curl::handle_setheaders( handle, "content-type" = "application/json" ) curl::handle_setopt( handle, customrequest = "POST", postfieldsize = nchar(auth_data), postfields = auth_data ) resp2 <- curl::curl_fetch_memory(conf_url, handle = handle) expect_equal(resp2$status_code, 200L) # Now everything is set up, a user can go to the login page of the # third party app: # browseURL(tpapp$url("/login")) # Scripting it is a bit tedious, because we need to parse the HTML and # submit a form. The `oauth2_login()` helper function does this. resp3 <- oauth2_login(tpapp$url("/login")) token <- jsonlite::fromJSON(rawToChar(resp3$token_response$content)) expect_equal(resp3$login_response$status_code, 200L) expect_equal(resp3$login_response$type, "text/html") expect_equal(resp3$token_response$status_code, 200L) expect_match(token$access_token, "^token-[0-9a-f]+$") }) test_that("oauth + httr", { skip_on_cran() # Not great, the OS should allocate a port, really... withr::local_envvar(c( HTTP_SERVER = "127.0.0.1", HTTP_SERVER_PORT = httpuv::randomPort() )) # Create the resource server rsappex <- oauth2_resource_app(access_duration = 0.1) log <- tempfile("webfakes-log-", fileext = ".log") on.exit(unlink(log, recursive = TRUE), add = TRUE) rsappex$use(logger = mw_log(stream = log), .first = TRUE) rsapp <- local_app_process( rsappex, opts = server_opts(num_threads = 3) ) regi_url <- rsapp$url("/register") auth_url <- rsapp$url("/authorize") toke_url <- rsapp$url("/token") # Register httr url <- paste0( regi_url, "?name=httr%20local%20app", "&redirect_uri=", httr::oauth_callback() ) reg_resp <- httr::GET(url) httr::stop_for_status(reg_resp) regdata <- httr::content(reg_resp) app <- httr::oauth_app( regdata$name[[1]], key = regdata$client_id[[1]], secret = regdata$client_secret[[1]], redirect_uri = httr::oauth_callback() ) endpoint <- httr::oauth_endpoint( authorize = auth_url, access = toke_url ) token <- suppressMessages(oauth2_httr_login( httr::oauth2.0_token(endpoint, app, cache = FALSE) )) # This will refresh the token automatically Sys.sleep(0.1) expect_message( cnt <- httr::content( httr::GET(rsapp$url("/data"), config = token), as = "parsed", type = "application/json" ), "Auto-refreshing stale OAuth" ) expect_equal(cnt, list(data = list("top secret!"))) loglines <- readLines(log) endpoints <- parse_url(map_chr(strsplit(loglines, " "), "[[", 2))$path expect_equal( endpoints, c("/register", "/authorize", "/authorize/decision", "/token", "/data", "/token", "/data") ) status <- map_chr(strsplit(loglines, " "), "[[", 3) expect_equal( as.numeric(status), c(200, 200, 302, 200, 401, 200, 200) ) }) webfakes/tests/testthat/test-mime.R0000644000176200001440000000022714172041777017105 0ustar liggesusers test_that("mime_find", { expect_equal(mime_find("json"), c(json = "application/json")) expect_equal(mime_find("blahxml"), c(xml = "text/xml")) }) webfakes/tests/testthat/test-mw-range-parser.R0000644000176200001440000000131414740243712021155 0ustar liggesusers test_that("parse_range", { expect_snapshot({ parse_range("foobar=1-100") parse_range("bytes=0-100, 50-150") parse_range("bytes=200-100") parse_range("bytes=x-100") parse_range("bytes=1-100") parse_range("bytes=1-") parse_range("bytes=-100") parse_range("bytes=0-100, 200-") parse_range("bytes=200-300, 0-100") }) }) test_that("intervals_overlap", { ok <- list( matrix(0, nrow = 0, ncol = 2), rbind(1:2), rbind(1:2, 3:4) ) for (x in ok) expect_false(intervals_overlap(x), info = x) bad <- list( rbind(c(1, 10), c(5, 10)), rbind(c(1, 20), c(1, 20)), rbind(c(1, 1), c(1, 4)) ) for (x in bad) expect_true(intervals_overlap(x), info = x) }) webfakes/tests/testthat/fixtures/0000755000176200001440000000000014740243712016720 5ustar liggesuserswebfakes/tests/testthat/fixtures/views/0000755000176200001440000000000014172041777020063 5ustar liggesuserswebfakes/tests/testthat/fixtures/views/test-view.html0000644000176200001440000000011414172041777022674 0ustar liggesusers { greeting } { user } webfakes/tests/testthat/fixtures/static2/0000755000176200001440000000000014172041777020277 5ustar liggesuserswebfakes/tests/testthat/fixtures/static2/static.tar.gz0000644000176200001440000000026614172041777022721 0ustar liggesusers0^K 0]E܀Mx;)eH⬈CK8!cpRpj+VϙqIXbcI2TJf65zs*gWC5|=K+D>|wHello world! webfakes/tests/testthat/fixtures/output/0000755000176200001440000000000014172041777020266 5ustar liggesuserswebfakes/tests/testthat/fixtures/output/webfakes_response.txt0000644000176200001440000000151114740246156024531 0ustar liggesusers> res fields and methods: app # the webfakes_app the response belongs to locals # response-wide shared data get_header(field) # query response header on_response(fun) # call handler function for complete response redirect(path, status) # send redirect response render(view, locals) # render template send(body) # send text or raw data send_file(path, root) # send a file (automatic Content-Type) send_json(object, text, ...) # send JSON data send_status(status) # send HTTP status and empty body set_header(field, value) # set a response header set_status(status) # set response status code set_type(type) # set Content-Type # see ?webfakes_response for details webfakes/tests/testthat/fixtures/output/webfakes_app_process.txt0000644000176200001440000000101614740246160025204 0ustar liggesusers> proc state: not running auto_start: TRUE process id: none http url: NA fields and methods: get_app() # get the app object get_port() # query port of the app get_state() # query web server process state local_env(envvars) # set temporary environment variables start() # start the app url(path, query) # query url for an api path stop() # stop web server process # see ?webfakes_app_process for details webfakes/tests/testthat/fixtures/output/webfakes_request.txt0000644000176200001440000000151714740246156024371 0ustar liggesusers> req method: get url: http://127.0.0.1:3000/ client: 127.0.0.1 query: headers: Host: 127.0.0.1:3000 User-Agent: It is me, libcurl Accept: */* Accept-Encoding: deflate, gzip fields and methods: app # the webfakes_app the request belongs to headers # HTTP request headers hostname # server hostname, the Host header method # HTTP method of request (lowercase) path # server path protocol # http or https query_string # raw query string without '?' query # named list of query parameters remote_addr # IP address of the client url # full URL of the request get_header(field) # get a request header # see ?webfakes_request for details webfakes/tests/testthat/fixtures/output/webfakes_regexp.txt0000644000176200001440000000007514740246156024171 0ustar liggesusers> new_regexp("^(foo|bar)$") "^(foo|bar)$" webfakes/tests/testthat/fixtures/output/webfakes_app.txt0000644000176200001440000000114514740246156023456 0ustar liggesusers> app routes: use * # add etag get /api fields and methods: all(path, ...) # add route for *all* HTTP methods delete(path, ...) # add route for DELETE engine(ext, engine) # add template engine for file extension head(path, ...) # add route for HEAD listen(port) # start web app on port patch(path, ...) # add route for PATCH post(path, ...) # add route for POST put(path, ...) # add route for PUT use(...) # add middleware locals # app-wide shared data # see ?webfakes_app for all methods webfakes/tests/testthat/fixtures/git-repo.tar.gz0000644000176200001440000006217314740243712021606 0ustar liggesusers5eX8**P D#EP"! )"|E+Vb⦆`;k{ow. ~=Arb{YYY9c\⋵#femkgoxK.%$vH‘%\~'pj7 Op. 1;)å2V_0lemhoU9"!+pzGk;k5W"xpD| M/I2y"I*[6H"!Y4y|*L"il ?I`pR#1.JqQ)#d8FII|!,2Xbi 80Y-Uz|p'2< 񏦙ʗHD5iQ #B=W=oec_G;:C3gsls|!^;8U#eEu|O[[[+?w\B!;bJù|&KK1hc&*aD,~U 7?Y$J?rd :wb6'2!:mmm/C 7m,/KvĠ L&E*?-6*ǝV<nílG)ތ+Opy8/љd`o:SH-bMYf ef`֖bPiRg@15:9ɀT3*V[8Tqb;>Pat:M dBVLDf/h\OØE8fbi4-& gMm<4ZӲv?`0)HdKZ[[ԓvjo8SOJap c@yc}ةlr@lЕlBW \ d%RK@r@ Q\0P F9O"JPPA?AiRQq݄8"(! PBJYcL&zG.$8;}>.@;JPP%l!'%ZLE O21>B1!A l)QA %TuP{ Hep)5a0[f$&Bs xbw$%d!64@!M5ǝ24p~K#LM  PHV+@,8b S"˔Hsqq%i<>F:(BSAGK3MՕ} hs6.es^ NV@1*}WQ@1T(!/4qP"@>и b1Ӕ`!*Fk* '%@ "A ㉯DoVeNV&t W(%(9<N_ # *a`H,5drc#f?+W 3 yyD`ITj tXHN7q%ACI$duE?FYʆ=T|jrԩAX?*)JWwڔH&49x v%AbHJ.4:]u&!2 #I!B"]T!A5-R ґb\"@'5.Fabρ )lF'5]lPTpI0*3T_MM8+3h,@s{` -Sfj+ ѳ~ 8<3U #f A6  S$ YBR6*zҊ fh@G CT$Д @)dMǨ $DD)(J+;'I('Ɂ(mP'J5/%zrY0`~`P wēBafX$OhX@yE2@(8kIlMq>@/92 FK7`|^4SA T^FPR q|0:O8D,%>)Xpz=8`< Qb@!pr,Ey|D!΁hI&1d Ra:@9#h2|0)2A( Pm+$39la|IF,BSIK@2ᛱ Xpa5a"PՒ4^JT5B@ ?ҌL_Q —{E= -BQ:`oSkb+A$] $HN!b>W>*XHؐQaMDTHRz: 6Vgt!C6moQDsalp:9A.0/"8QHB"@ ͷZzR 8T`]Ɛ?,0;,@L:*$6TADo!A] ؤIiIOf``ф+e!*'!gARʡP(dX4J;03"'Tg5JRq@}&é x_S1$ӣGAIx'b dYY<7@SܘW7-Un[E޵$߳ľ QG$$i4O 8½<!D'/ug!OI Ga2 Bex|!Pɾ2Ȳ҆ ~4DYMiRT7 %@]Y Pu.aC{0IOBB`pKI8ͫ^PA@ IAMh Bo@u(ȶFJ_W6OȐsh[J1(QC^_Jq`u`i?;;+:_;+o.QLl 1S -,QRiG8> ڌ:ٯ:s6@F՚Tf_3. tW&<\ C%/`бX.ekJBe7*Z)hJ3i#`BIq f-% MR9No WԼHى"{X`B0l6RK'٠QF'INHr/($} \JHeHL%!0E #u`ĆS7_+K@o1Ul|DH }M 4UטQ9`>FLD UoBZB8dSi}c0(APȿ,er(4P}3z l &C4u`Тh*:,S@p BХUMc ֤ϖ6:<9LjL$A5g`ݻC@A;0!P2JM[ | b XESPn&"??+#K-?h` &0hCU |ҞZ&AU[ .7`#RXLd@ Zl0F3@dv! seBe iawƣt[5hS+I2 }T$$lĚ b!..4M[K=qJ6\ mdh(T/`˅xRy"V9 1ԳxDIXʶp+&]<TD@Vn.IQ"pL&}YKڐ TIB'`Pδ>9-0+ ̚iC'VDހBh6TuZZ . @$˸cnntM^f ǡx3F-zmT'`1tHh@ X'̈́"}@A;'!TB &U* wR*'AxʷwǮԈȴt(qmehFR뫍^J7 vQ%"}a BUAtUU*%T.KJae\l Y #@q2u<)ГlrffDGr\tJE2#w"ps˒&HN.׹u.QWت/?97]шw4@% =O #|RPF˱L)9HT"! 3OBfKE<*(*LmKdt'`2+"Ԥ*X 0Mp(4<-_o [=ϕwL]SdTTQ} xh^GHW 5#'=$=V L2R|w+fz2C?S z9M,tG{yIK(/ Cf#!S*_])LbC3l2T(DD0q&1c`ˀ( 7p/GlG?2@6VGEE HQPP`"ʽ`&R0K8#Mz <%8ޯ i02B R;|&X UADJdHkD6H>-Xh& w*AXkXt,g;T+)7,凂` CpRZJ9Q)-3Y;z2; p3uCLP3\%g0|Vʊ0 m6+rTF vwSD$p5I$"rl :>(mwp7Dy6>F4ZC|V";"8fBx ie{}jb ,0M,PLȨf1FQ Ć9a0!Zk_,a NI%5,`k CY#cUCc1TµyH>8)Lwp@71|!`ƤRI !V+EV-z ف 2 (\ v!6WH*cz z?烨D#*FD MHZ*9AE1)%&<_`ƿC=ݟ{xbߴKF䀇L$VTD(]*IZݔhg. ?oe4`_o{_9aj_wlIC8@H~吮ɯɯ"~XO_hCj~ ~S×pmD08%24HB2JG2ɢJ"|VfzR~O{t*H#6mUGQ~tm3&DI0:&砬&r|hk%?nT6rg`&V& X1x֊wGW Uo T FX@~ `Ny e3HJ1x@V~'kZ.ߨ~T? IRWpP$z,C T 0huY{ _ 06YZ!-~m "` >`#LT\*s\V)VBjfD 9$x % (AѸG|! vvK^ł`^`l@Z#L6T; SMQ0󕃀)9] FRN(QaF@ BTTTmfA/2k)R$0XlFY'CC%ldWdRNiQT_Y[fTV jN-r+X TBT@ߡ.R )@_IJP=>Jv4Tg ($SnDt#*)cEiNByD%źb{V'SV'^ѵ&T)"ĉSz,4nWqxPPRLXnTW$ޔV9u!}Vtۯ—J[2Ni~5\Tܯhkf1Km{0h!BZN)2A=N\כ<ǟW{[[Ǻ~pljPZxh _8OUB8Ylp\;B (<EB4\@G(ї)i+YʄPi MwB;O&ԅ %D~B 'wي$&.Jj'`j/=3%P-A/ =Vj6FPE'Njc ށOCk]@2&_l yam!Vc@`|ϭuJ%pH/85E31ʢ0Ŗ ΕEf=Щ~/@T5";|*QD I"D\hZ5J݊BU"%/x ̿Bp)5`=j`~1-d".uFJ3X4י8lCT_:E^$/Qqf#//ʝ(!3jR`a k#CS;UaE 7Aɚ}!VG?#4l.5)ǘDVp䵃-T'rYp,NCJ 'F@/2-V M."jEEqA*@7Q2DC@"0u]xGD 0T* K1ٌZ}q\TEr]Ef%t\sG j:+" Syc!aP2<do$ K5%DRex..2a'CLd>ԁJNQ$׏ [d4@4*7E463 ںRKPכ:n2j/UwVXh߯}=֡n/g;.b@|d&.8ȴ@""nFCd8QhCc=Ζ@uk.ΤوdP}-S"h:Ne񋙱bDð?UBj M_Rjہ`/\_R?)mm︾3m#;ʖml8v3džslq'. Z oo_w;{0^:y{v{nGp7 ;`7u$&n[pܾ[5mnpk-ۃ;nWpہ[܍ܚn p7ܝ ~Op;knnnnۀ ܦ7KmNC4`E`Q5p޾tHc nTL30gVi$Y=`NvvdcKj{U H 3Jؾ䀷5nC-W]^Q=w|wQMhÍ.0j6>rnV74hN S7.叴<7ޠToa5SrrIґEWUk.8D-u߃JtKmh~zf:Mcwm7*cٝuqߗIM!'<KM-~; }}1)sJcBoL<-YwA+2<8a]fҠ.y7>{}YޅiOJ=2^ȪN^M{X 33" Sǿk޿]Ō|۴i+gOjyL1}'_>O.n jLif9~I9asL/9`ne7ZXW\NJK6ٷg+&/d(JrJ@' /dtwзzĦU\u8LީŦ͡##{_!B~ H/e + Bs]ẍ˃s.K_~Kwm faIN>kt+0X>O/KNϸxyzrιg$oXˑsǗܥ_ hu;ngؽE]^@>몓 }քF+Ch++8IYkr|'u[y`0k `I|:zudzCLn=ak1/63/@#,kw!e;Uʳ:#f̲ܻúʏw鴫CLq3=I(zkeO5uz{EIFZe ܝ ˦]G3LTtf@}fGk78aڶfw~sgx^:jQ,mD)[Tv\ErvvȥI$fjev}YG< vˁYyyz¤e:;DT T dY^J:e][< ޗGީ [H!6c6a>[_};>쫭z%V:j[̵VSqJq#'Y =l(;{F}4ɹ.1˚o;vr3 w#Z Ѵ<{lGkO6=/t\+?:*!{/ =\xql{֖OXrnMwe[΢_xPrE/AJh֔A=,ʯ|-ZTHJ vsHcϵY{w=NOnR|rek#5"^oU9d#~8uĶ)65 H|woљ{wIԼǮ^MG,Ne  9oxc|XЛO̪jƵ\kŴo = NX~I@3ƞK-[FLRM&n*]ci4uS] 嫚Qx3F׌c'Slȫy_?q}6gk^u0wW]fU+5/8dqz3sO1Orjicg?zk4ںI*hCnEf~8g#=.;c]/8{gk=QW]jsJc'/h%f89(HsVzZI;+wqja3}\Wvr·m'&:w+&|T3YgM UjIQg:!t820~/ca-hI8 0ٜ!6/ch6bZRD\0-ݒCekzmu#dTzzq¹m԰17As羽`6] /js޾].&,@NlYX%+~z?tm54f#iw krH_ Χunἤ<F9@Z}8HZsgll?!O%UWf {`NF_MO[Fú=VL=s٬+{6}77e2PGGȭy֊#b[uzLky.bVм-DΫvm~܋m."1ׄ5*gJ:]}Nc^u.2^6׹_i۰kq~ێK9o:\gFe7Dk<{ <ѻ<6fj;6G$&}ݦ7>0fo[pr_%=-B7]SZ2ͧ=,Z}(ש!#:aXv7Qb[Eŭ`uf9&* kCGT9xG/4Eů.xWdt7{{y&p)k6蜨Vۃh7\Sm&O~E /+'fMD";sox; 6}ƇOO՝2R'oc>{3[%L{|ӊUFz8`p6vgs~#!waۋrg+ZIۏJ?7>tWͽv H}D ]L*q,򾍧Uj2ƌENK0.\.ZȟBqL?հforlw8XtuҼޚirO1rEЙlFg98t<>0kP\WNnPu4 ,G{RxlADA#s^ըM7F42zՏfv<0Scqrcږ-S;H{5md͍,<۳eFۖw݌fl|њ7qv^:<7Mtӭo~p+wNbކų<2'cƭ|'YX1ƅYx;]Fo1Yz\ƒA< g?i“q6nq'ge>4{-˸ 6Yf#LJXաWx&eɖO]f7}24;ͣkUG.kqM#gڜԄf{.|婳NWug/˝9˽<yz ۭf:[nXge ^/٦ɺeqJ'tʃSo֌Nh2ߟ`sl/Q.kk.dz4oGbځ[}8ճyEfm;w+`.F"CzatZ3f.U)m[]iՈc !~[\[~ŮGHy4=ŧ>~3̻.V|dOQzèOo_[&BӠvTw'/n9v |`"N"M}Y]?qR`;nj6 }&];8NMўC6ʺu4;m6X .^?y["O}7)pqݻ3)Vף/(Yi$f}h:QEUjt;$7ieF˞/yXe5m@Zw֜oq̢]hGk^/{tn"-M58iq4(~CFYKN#f:u(зTJ85)~1FA}9ڢiNVǼݰt+KMՌp@svksu o,htY|^i]ud{!mC[V8`ԱZwXx7RbmВwWr&oM?FًnG{2;w0q+}y wԜ}[T uۀNswlX[3ߡ[Uu+mLJ:YY1-673ePv-JWw=a:Ž ?$|n~X+Ŧo\¢ne-r. tcL1F;էx{ՀkmO7eK>5([4-~MgCN0-Yazt:|̲o#~\;KiKҟ| ݚYtD]Ϧ7[jCV?_\ɦO=lЃc ]hGܙ$7JC w-,w]4M u9On2G6=&9سL7su)R69]kukFf܄C>Y}*}>kgy齚ի@KeD.;$>mVĠ.f,K5i6Lk)}F/)Ó,L*V?;W.K_&|8)-#NGWO?env|✼sQ 6;p;k]U٩\[Npl+ٖ2nXc;y ZU!}@;}dߧv<],Pص-RGfϬY).8r&ʸZm& #M1-/gY.th@inr5]v6\nf gjjqmuypȼc@fΌx+n;|mT1{1lp1gc^Iw:qW}9w$øjuBc¦Y?LZ;,ęYeMh3y9°]Ԗk5EjX؍~<[r -"H?UL^]6:'%CXF;iO4w{їouh;meo\ Y`hF4nd#UkO.pQt]\][dx{?e[lΥ^3m72Qճׇz8u쒫^^A=jqݻ[-޽3bnէݚsG74d)J)nO3L쪑䈋I7֝9 *?21MoZ93 +>ezB,F:Nm&.}f _G];FI6}yݑ]=s-XqÈ#?Ӽqm{w[l e0o\tKxWxвV!v5^d%IjFoםch#iN]9g a{tf,?Xڀ->AfݓL4J\aڑww1hMuҳ:{&=̽ث-_95_iLׁ1 &7XgHzIlـ%WN4ǛhΣ bu2wۭ@ާoPlⷎyU9+ّg;@N+^;]]K:!fQ=߫\9&k{[!|l_рM7t`vna ! 6j8׉S=&VU!2L:_*=4Bm}ۜ4}_Z?n-w*Ϙ{Cjlbo;KenenTc {s+6woAc͞a;fiq̗7_=rwpI9:uOj׼3=5d^:d] U #CrNgei>bAk󂬋onthS 1kd:;n񨳣9]^=ɀf_Z֎kٛ 1fT&v1TV#=|thnGC%)0KmΚGk>3CCr'wwS>`CoaD]Fzϫ~zjߞ!hGқ/KoSV+u`Ω+NmgTm1yN^<ී?Kosmh' 6kW4h^GeynyjZ6jt۸~-\}ƦLj ܴ)n2Xb^,dnΓ*xܣʼn3;iF ߢ|ґa![[7x ]}KN2q'(fu;i26 ,(g'2<;o+&0hgIQBIKeeZ<{E/]#8Q3ɡWjZ;W8mGigծR?xǸ֙ڷMQF V3Wv7ixvܸ݊;V~#ǭiU.oP~c7%OO^? +//>k̰R:o=xS3VX{grqcwQu@/(uVض3U%-Uu06>{e##Lv˟$w~2^`Reߋza-k-Yse==woߧtY~m[n&_+00tV.`NxԹr 2kf֮CX'ћhHk΁Zܴ5GiR{_'-dlSv;-=u4dw鷵ˌn]Lm= K[,55?:ܗ Gq0mTp$%>jרR5_02U\8et%r=wV%w南_L9G=1I^MF}t94g&Jx%z:ҕL㸌_a^yVs6TD{72;Wu\7}S?yM{#oI-w誴+Fݖ3C ׮Kyl So&<{UőS%cYso=ҞXvcE=7J{D;渞?[dߍc>?{ջHlw&$=iyuMձw|svgJ7n;i}eFZS?󝋻F5 nzr@|k,cfBIͺZ;H3vd9o^NBU]+E/ڏ(2!u#%4ۮYldyM'O 7$onGUFܩUuޥcG!Σާ _%ޤcMw vzKe/^sc.{LsG)z6'ǚ#`λ4"k߼ڲ5$ A%I IR˕ IIM JP@@ 6 H5LswܪyoU~?NwzɥKF[xHN'Gs53WLSܩ5xʷ:W'\WZYb.1&E k1] d"-͝O0lB^') pX7';7|v<;6@̛ʅlE"IG+7fRW5`DK.'N.]ȋjįQB2>y <5̆{݄Ìe\r*Ye>dA*3w :5ZX;LȲ|۟ RT 1e  ۷ 1+PyHBj]7 }ԖQݏ#rָ-O[*bNr>9v#MHC|!Xh>G؛@yr~?TPL\0̑aj&eP֚*>'M\YN1JjaDTTGCծӼ[Uzt6U>d È,:+ IwH- q6).6tOfm1dR9NFaf@WZ0W 1E t$*K o`a:=_Ycl{g<>9q#[ XfbIQ,s* [5oק& z[7HT}CV9晷q7팞M !ttȠVaӧ\yf3Pey8?зڊg8ުD{t$'mc"3{X(+qd9Y>eQl}ZBrR-l}cQإn3R˲`=B`gU(~(5=PkEf+Мܮly0/(?6Ojl>lZ^%>_|^wpu~Pb'ACDS9ok~j5'[e:~x-Z"h`%MB]+ KdΕq/nawƻh_#CD)uqQ+/aeP+Fk55x^Vw7onoK)CJθ.<Dmx<( `ߩvҦFX~H&Ze8fwۨ"{YiAQWyIBEՏd$EUC%Ә5E24Fk>c֥dM`/!wʾM8\;t b%1%-E}Ij<_ &VHzB#I{=eb0DY} L czS33=Fm쇨HhٔX4t.}_\g%kmu_pi1 &5wLM 2:]]٬5Rk'KG/e_(XVu]S8W'Kw"GCEKEЫc$#Brs arfMֽX ##,l5cDD)i@? LQ͈1Q[/6-:BU|\>nGd=:_lix54+BJ),j2S&A;=LFZ% [<nGyrE07SU!$<'^`Dx#-SgJq0Wo—q*r#_bක!U+v,ssblNw<}GG[\)+OoĮ 4İ\4m2+[h?eָNJis  #,sK HegY2iIP _)vg!@aX c 6]_?l4އO kXc 8Dgxw߲eodaO0x,z6:[H0 3aYEc} 6 4L>EE_(A z [ُ ςtbpv4I<ݗEl9_eĀ -9x4"߱lK(ƄUwAY7;YҢw.Ѹ5u^g\"=7u: *)*}|v[ol23-FflZ+kert-1Pچ{*XB`eTZ t&i&k1ߓnTt&<䘫#.z055B/ku_5,Q hTSڭڡ4\5'O߬{1/7]gEVpc^.f(\8s! 3|ZzW9måyIffW3%k=8*]/`p"a+}t L٦/ULp+`&N>l(Iݲҁiޮ%o^|[L/h\_׼ȭ$iU4$%NfӇӭKgUP`N2ޫ El [~vػxq^NcwF?耛!$QM&+7~Tx'eSY)I{fT!FrZjE)s/0ݒ7=ոQrkr|dE.s"@ĝzxg#kP%`-A"@5\mx?Ƒ15U uk@ɇKˁ$}OփB+qo=16t6E?xe%zkjHT]tZbOkHgݤ,}'s rno'|0̢A'Y&k=FˉxaAc"Y1%c73XDt$.j7Q}L)5Sb*4T6}f܌D1U%_,pr3i4hzq!g,6b9s?4Ufh yS0Uf`%g`42-H4cd% 20@<9@E@Z` -@q:\2@_MÁϦ@60 Y@}t@/7@4Z7^@ 6@_p;'4c#~@ ;Z \rhM)khmC jbY9@OQF.zr3{2/V+zWǘ+~ů+g92webfakes/tests/testthat/fixtures/static/0000755000176200001440000000000014172041777020215 5ustar liggesuserswebfakes/tests/testthat/fixtures/static/subdir/0000755000176200001440000000000014172041777021505 5ustar liggesuserswebfakes/tests/testthat/fixtures/static/subdir/static.json0000644000176200001440000000002114172041777023660 0ustar liggesusers{ "foo": "bar" } webfakes/tests/testthat/fixtures/static/static.html0000644000176200001440000000006414172041777022372 0ustar liggesusersHello world! webfakes/tests/testthat/test-app-process.R0000644000176200001440000000355314740243712020411 0ustar liggesusers test_that("error if cannot start", { # does not start before the timeout app <- new_app() app$listen <- function(...) Sys.sleep(1) expect_snapshot( error = TRUE, new_app_process(app, process_timeout = 100, start = TRUE) ) # errors before/while starting app <- new_app() app$listen <- function(...) stop("oops") expect_snapshot( error = TRUE, new_app_process(app, start = TRUE) ) # sends a different message first app <- new_app() app$listen <- function(...) "foobar" expect_snapshot( error = TRUE, new_app_process(app, start = TRUE) ) }) test_that("get_state", { app <- new_app() on.exit(proc$stop(), add = TRUE) proc <- new_app_process(app, start = TRUE) expect_equal(proc$get_state(), "live") proc$.process$kill() expect_equal(proc$get_state(), "dead") expect_output(proc$stop(), "webfakes process dead") expect_equal(proc$get_state(), "not running") }) test_that("env vars", { app <- new_app() on.exit(proc$stop(), add = TRUE) withr::local_envvar(list(FOO = "foo")) withr::local_envvar(list(BAR = NA_character_)) proc <- new_app_process(app, start = TRUE) proc$local_env(list(FOO = "bar")) proc$local_env(list(BAR = "{url}")) expect_equal(Sys.getenv("FOO", ""), "bar") expect_equal(Sys.getenv("BAR"), proc$url()) proc$stop() expect_equal(Sys.getenv("FOO", ""), "foo") expect_equal(Sys.getenv("BAR", ""), "") }) test_that("env vars 2", { app <- new_app() on.exit(proc$stop(), add = TRUE) withr::local_envvar(list(FOO = "foo")) withr::local_envvar(list(BAR = NA_character_)) proc <- new_app_process(app) proc$local_env(list(FOO = "bar")) proc$local_env(list(BAR = "{url}")) proc$start() expect_equal(Sys.getenv("FOO", ""), "bar") expect_equal(Sys.getenv("BAR"), proc$url()) proc$stop() expect_equal(Sys.getenv("FOO", ""), "foo") expect_equal(Sys.getenv("BAR", ""), "") }) webfakes/tests/testthat/teardown.R0000644000176200001440000000002614172041777017021 0ustar liggesusers try(httpbin2$stop()) webfakes/tests/testthat/test-httpbin.R0000644000176200001440000006510614740243712017627 0ustar liggesusers # HTTP methods ========================================================= test_that("/get", { url <- httpbin$url("/get", query = c(q1 = "one", q2 = "two")) handle <- curl::new_handle() curl::handle_setheaders(handle, "foo" = "bar") resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 200L) expect_equal(resp$type, "application/json") data <- jsonlite::fromJSON(rawToChar(resp$content), simplifyVector = FALSE) expect_equal(data$path, "/get") expect_equal(data$headers$foo, "bar") expect_equal(data$args, list(q1 = "one", q2 = "two")) }) test_that("/post", { url <- httpbin$url("/post", query = c(q1 = "one", q2 = "two")) data <- charToRaw(jsonlite::toJSON(list(foo = "bar", foobar = 1:3))) handle <- curl::new_handle() curl::handle_setheaders( handle, "content-type" = "application/json", foo = "bar" ) curl::handle_setopt( handle, customrequest = "POST", postfieldsize = length(data), postfields = data ) resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 200L) expect_equal(resp$type, "application/json") data <- jsonlite::fromJSON(rawToChar(resp$content), simplifyVector = TRUE) expect_equal(data$path, "/post") expect_equal(data$headers$foo, "bar") expect_equal(data$args, list(q1 = "one", q2 = "two")) expect_equal( data$json, list(foo = "bar", foobar = 1:3) ) expect_equal( jsonlite::fromJSON(data$data, simplifyVector = TRUE), data$json ) }) test_that("/post and multipart data", { on.exit(rm(tmp), add = TRUE) tmp <- tempfile() writeBin(charToRaw("foobar\n"), con = tmp) url <- httpbin$url("/post") handle <- curl::new_handle() curl::handle_setopt(handle, customrequest = "POST") curl::handle_setform( handle, a = "1", b = "2", c = curl::form_file(tmp, type = "application/octet-stream") ) resp <- curl::curl_fetch_memory(url, handle = handle) echo <- jsonlite::fromJSON(rawToChar(resp$content), simplifyVector = FALSE) expect_equal(resp$status_code, 200L) expect_equal(echo$form, list(a = "1", b = "2")) expect_equal( echo$files$c, list( filename = basename(tmp), value = paste0( "data:application/octet-stream;base64,", base64_encode("foobar\n") ) ) ) }) # Auth ================================================================= test_that("/basic-auth", { # no auth supplied url <- httpbin$url("/basic-auth/Aladdin/OpenSesame") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 401L) expect_equal(resp$type, "text/plain") headers <- curl::parse_headers_list(resp$headers) expect_equal(headers$`www-authenticate`, "Basic realm=\"Fake Realm\"") # correct auth handle <- curl::new_handle() curl::handle_setheaders( handle, "Authorization"= "Basic QWxhZGRpbjpPcGVuU2VzYW1l" ) resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 200L) expect_equal(resp$type, "application/json") expect_equal( jsonlite::fromJSON(rawToChar(resp$content)), list(authenticated = TRUE, user = "Aladdin") ) # wrong auth handle <- curl::new_handle() curl::handle_setheaders( handle, "Authorization"= "Basic NOLUCK" ) resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 401L) expect_equal(resp$type, "text/plain") headers <- curl::parse_headers_list(resp$headers) expect_equal(headers$`www-authenticate`, "Basic realm=\"Fake Realm\"") }) test_that("/hidden-basic-auth", { # no auth supplied url <- httpbin$url("/hidden-basic-auth/Aladdin/OpenSesame") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 404L) expect_equal(resp$type, "text/plain") headers <- curl::parse_headers_list(resp$headers) expect_equal(headers$`www-authenticate`, NULL) # correct auth handle <- curl::new_handle() curl::handle_setheaders( handle, "Authorization"= "Basic QWxhZGRpbjpPcGVuU2VzYW1l" ) resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 200L) expect_equal(resp$type, "application/json") expect_equal( jsonlite::fromJSON(rawToChar(resp$content)), list(authenticated = TRUE, user = "Aladdin") ) # wrong auth handle <- curl::new_handle() curl::handle_setheaders( handle, "Authorization"= "Basic NOLUCK" ) resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 404L) expect_equal(resp$type, "text/plain") headers <- curl::parse_headers_list(resp$headers) expect_equal(headers$`www-authenticate`, NULL) }) test_that("/digest-auth", { # url <- "http://localhost:3000/digest-auth/auth/user/secret" url <- httpbin$url("/digest-auth/auth/user/secret") handle <- curl::new_handle() resp <- curl::curl_fetch_memory(handle = handle, url) headers <- curl::parse_headers_list(resp$headers) expect_true("www-authenticate" %in% names(headers)) creds <- parse_authorization_header(headers$`www-authenticate`) expect_equal(creds$scheme, "digest") expect_equal(creds$realm, "webfakes.r-lib.org") expect_equal(creds$qop, "auth") expect_equal(creds$algorithm, "MD5") expect_equal(creds$stale, "FALSE") auth <- list( username = "user", realm = "webfakes.r-lib.org", nonce = creds$nonce, uri = "/digest-auth/auth/user/secret", qop = "auth", nc = "00000001", cnonce = "0a4f113b", opaque = creds$opaque ) hash <- function(x) digest::digest(x, algo = "md5", serialize = FALSE) HA1 <- hash(paste(c(auth$username, auth$realm, "secret"), collapse = ":")) HA2 <- hash("GET:/digest-auth/auth/user/secret") auth$response <- hash(paste0( collapse = ":", c(HA1, auth$nonce, auth$nc, auth$cnonce, auth$qop, HA2) )) authorize <- paste0( "Digest ", paste0(names(auth), "=", auth, collapse = ", ") ) curl::handle_setheaders(handle, authorization = authorize) resp2 <- curl::curl_fetch_memory(handle = handle, url) expect_equal(resp2$status_code, 200L) cnt <- jsonlite::fromJSON(rawToChar(resp2$content), simplifyVector = TRUE) expect_equal(cnt, list(authentication = TRUE, user = "user")) }) test_that("/bearer", { # no auth url <- httpbin$url("/bearer") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 401L) headers <- curl::parse_headers_list(resp$headers) expect_equal(headers$`www-authenticate`, "bearer") # bad auth format handle <- curl::new_handle() curl::handle_setheaders(handle, authorization = "foobar") resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 401L) headers <- curl::parse_headers_list(resp$headers) expect_equal(headers$`www-authenticate`, "bearer") # correct format handle <- curl::new_handle() curl::handle_setheaders(handle, authorization = "Bearer secret") resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 200L) echo <- jsonlite::fromJSON(rawToChar(resp$content), simplifyVector = FALSE) expect_equal(echo, list(authenticated = TRUE, token = "secret")) }) # Status codes ========================================================= test_that("/status", { codes <- c(200, 301, 401, 404) # get for (code in codes) { url <- httpbin$url(paste0("/status/", code)) resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, code) } # post data <- charToRaw(jsonlite::toJSON(list(foo = "bar", foobar = 1:3))) handle <- curl::new_handle() curl::handle_setheaders(handle, "content-type" = "application/json") curl::handle_setopt( handle, customrequest = "POST", postfieldsize = length(data), postfields = data ) for (code in codes) { url <- httpbin$url(paste0("/status/", code)) resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, code) } }) # Request inspection =================================================== test_that("/headers", { url <- httpbin$url("/headers") handle <- curl::new_handle() curl::handle_setheaders(handle, "header1" = "this", "header2" = "that") resp <- curl::curl_fetch_memory(url, handle = handle) echo <- jsonlite::fromJSON(rawToChar(resp$content), simplifyVector = FALSE) expect_equal(echo$headers$header1, "this") expect_equal(echo$headers$header2, "that") }) test_that("/ip", { url <- httpbin$url("/ip") resp <- curl::curl_fetch_memory(url) echo <- jsonlite::fromJSON(rawToChar(resp$content), simplifyVector = FALSE) expect_equal(echo, list(origin = "127.0.0.1")) }) test_that("/user-agent", { url <- httpbin$url("/user-agent") ua <- "i am libcurl, that is my name" withr::local_options(list(HTTPUserAgent = ua)) resp <- curl::curl_fetch_memory(url) echo <- jsonlite::fromJSON(rawToChar(resp$content), simplifyVector = FALSE) expect_equal(echo, list("user-agent" = ua)) }) # Response inspection ================================================== test_that("/etag", { url <- httpbin$url("/etag/foobar") resp <- curl::curl_fetch_memory(url) headers <- curl::parse_headers_list(resp$headers) expect_equal(resp$status_code, 200) expect_equal(headers$etag, "foobar") handle <- curl::new_handle() curl::handle_setheaders(handle, "If-None-Match" = "\"foobar\"") resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 304) expect_true(length(resp$content) == 0) handle <- curl::new_handle() curl::handle_setheaders(handle, "If-None-Match" = "\"not-foobar\"") resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 200) expect_true(length(resp$content) > 0) handle <- curl::new_handle() curl::handle_setheaders(handle, "If-Match" = "\"foobar\"") resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 200) expect_true(length(resp$content) > 0) handle <- curl::new_handle() curl::handle_setheaders(handle, "If-Match" = "\"not-foobar\"") resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 412) }) test_that("/response-headers", { url <- httpbin$url("/response-headers") resp <- curl::curl_fetch_memory(url) expect_equal(rawToChar(resp$content), "{}") url2 <- httpbin$url("/response-headers", c(foo = "bar")) resp2 <- curl::curl_fetch_memory(url2) expect_equal( jsonlite::fromJSON(rawToChar(resp2$content)), list(foo = "bar") ) headers <- curl::parse_headers_list(resp2$headers) expect_equal(headers[["foo"]], "bar") url3 <- httpbin$url("/response-headers", c(foo = "bar", foobar = "baz")) resp3 <- curl::curl_fetch_memory(url3) expect_equal( jsonlite::fromJSON(rawToChar(resp3$content)), list(foo = "bar", foobar = "baz") ) headers <- curl::parse_headers_list(resp3$headers) expect_equal(headers[["foo"]], "bar") expect_equal(headers[["foobar"]], "baz") handle <- curl::new_handle() data <- charToRaw("{}") curl::handle_setheaders( handle, "content-type" = "application/json" ) curl::handle_setopt( handle, customrequest = "POST", postfieldsize = length(data), postfields = data ) resp31 <- curl::curl_fetch_memory(url3, handle = handle) expect_equal( jsonlite::fromJSON(rawToChar(resp31$content)), list(foo = "bar", foobar = "baz") ) headers <- curl::parse_headers_list(resp3$headers) expect_equal(headers[["foo"]], "bar") expect_equal(headers[["foobar"]], "baz") url4 <- httpbin$url("/response-headers", c(foo = "bar", foo = "bar2")) resp4 <- curl::curl_fetch_memory(url4) expect_equal( jsonlite::fromJSON(rawToChar(resp4$content)), list(foo = c("bar", "bar2")) ) headers <- curl::parse_headers_list(resp4$headers) expect_equal( headers[names(headers) == "foo"], list(foo = "bar", foo = "bar2") ) }) test_that("/cache", { url <- httpbin$url("/cache") resp <- curl::curl_fetch_memory(url) headers <- curl::parse_headers_list(resp$headers) expect_true("last-modified" %in% tolower(names(headers))) handle <- curl::new_handle() curl::handle_setheaders( handle, "If-Modified-Since" = http_time_stamp(Sys.time() - as.difftime(5, units = "mins")) ) resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 304L) handle <- curl::new_handle() curl::handle_setheaders( handle, "If-None-Match" = "some-etag" ) resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 304L) }) test_that("/cache/:value", { url <- httpbin$url("/cache/10") resp <- curl::curl_fetch_memory(url) headers <- curl::parse_headers_list(resp$headers) expect_equal( headers[["cache-control"]], "public, max-age=10" ) }) # Response formats ===================================================== test_that("/deny", { url <- httpbin$url("/deny") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 200L) path <- system.file( package = "webfakes", "examples", "httpbin", "data", "deny.txt" ) expect_equal(resp$content, read_bin(path)) }) test_that("/gzip", { # curl seems to ungzip automatically, so we use url() url <- httpbin$url("/gzip") con <- url(url, open = "rb") on.exit(close(con), add = TRUE) echo <- readBin(con, "raw", 10000) expect_equal(echo[1:2], charToRaw("\x1f\x8b")) json <- readChar(gzcon(rawConnection(echo)), 10000) obj <- jsonlite::fromJSON(json, simplifyVector = FALSE) expect_equal(obj$path, "/gzip") }) test_that("/deflate", { url <- httpbin$url("/deflate") con <- url(url, open = "rb") on.exit(close(con), add = TRUE) echo <- readBin(con, "raw", 10000) data <- jsonlite::fromJSON(rawToChar(zip::inflate(echo)$output)) expect_true(data$deflated) }) test_that("/brotli", { url <- httpbin$url("/brotli") con <- url(url, open = "rb") on.exit(close(con), add = TRUE) echo <- readBin(con, "raw", 10000) data <- jsonlite::fromJSON(rawToChar(brotli::brotli_decompress(echo))) expect_true(data$brotli) }) test_that("/encoding/utf8", { url <- httpbin$url("/encoding/utf8") resp <- curl::curl_fetch_memory(url) expect_equal(resp$type, "text/html; charset=utf-8") ptrn <- as.raw(c( 0xe1, 0x8c, 0x8c, 0xe1, 0x8c, 0xa5, 0x20, 0xe1, 0x8b, 0xab, 0xe1, 0x88, 0x88, 0xe1, 0x89, 0xa4, 0xe1, 0x89, 0xb1 )) # On windows end of line is converted to \r\n expect_true(grepRaw(ptrn, resp$content, fixed = TRUE) %in% c(9085, 9233)) }) test_that("/html", { url <- httpbin$url("/html") resp <- curl::curl_fetch_memory(url) expect_match(resp$type, "^text/html") expect_match(rawToChar(resp$content), "", fixed = TRUE) }) test_that("/json", { url <- httpbin$url("/json") resp <- curl::curl_fetch_memory(url) expect_equal(resp$type, "application/json") path <- system.file( package = "webfakes", "examples", "httpbin", "data", "example.json" ) expect_equal(resp$content, read_bin(path)) }) test_that("/robots.txt", { url <- httpbin$url("/robots.txt") resp <- curl::curl_fetch_memory(url) expect_equal(resp$type, "text/plain") path <- system.file( package = "webfakes", "examples", "httpbin", "data", "robots.txt" ) expect_equal(resp$content, read_bin(path)) }) test_that("/xml", { url <- httpbin$url("/xml") resp <- curl::curl_fetch_memory(url) expect_equal(resp$type, "application/xml") path <- system.file( package = "webfakes", "examples", "httpbin", "data", "example.xml" ) expect_equal(resp$content, read_bin(path)) }) # Dynamic data ========================================================= test_that("/base64/", { url <- httpbin$url("/base64") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 200L) expect_equal(rawToChar(resp$content), "Everything is Rsome") value <- base64_encode("hello there!") url <- httpbin$url(paste0("/base64/", value)) resp <- curl::curl_fetch_memory(url) expect_equal(resp$type, "application/octet-stream") expect_equal(rawToChar(resp$content), "hello there!") }) test_that("/bytes", { url <- httpbin$url("/bytes/foo") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 404L) url <- httpbin$url("/bytes/1000") resp <- curl::curl_fetch_memory(url) expect_equal(resp$type, "application/octet-stream") headers <- curl::parse_headers_list(resp$headers) expect_equal(headers[["content-length"]], "1000") expect_equal(length(resp$content), 1000) }) test_that("/delay", { url <- httpbin$url("/delay/foo") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 404L) url <- httpbin$url("/delay/0.2") st <- system.time(resp <- curl::curl_fetch_memory(url)) expect_true(st[["elapsed"]] >= 0.1) expect_equal(resp$status_code, 200L) expect_equal(resp$type, "application/json") echo <- jsonlite::fromJSON(rawToChar(resp$content), simplifyVector = FALSE) expect_equal(echo$path, "/delay/0.2") }) test_that("/stream-bytes", { url <- httpbin$url("/stream-bytes/100") resp <- curl::curl_fetch_memory(url) headers <- curl:::parse_headers_list(resp$headers) expect_equal(resp$status_code, 200) expect_equal(headers[["transfer-encoding"]], "chunked") expect_equal(length(resp$content), 100) ## seed works url2 <- httpbin$url("/stream-bytes/10", c(seed = 100)) resp2 <- curl::curl_fetch_memory(url2) expect_false(identical(resp$content, resp2$content)) resp3 <- curl::curl_fetch_memory(url2) expect_identical(resp2$content, resp3$content) ## chunk_size works url <- httpbin$url("/stream-bytes/100", c(chunk_size = 30)) resp <- curl::curl_fetch_memory(url) headers <- curl:::parse_headers_list(resp$headers) expect_equal(resp$status_code, 200) expect_equal(headers[["transfer-encoding"]], "chunked") expect_equal(length(resp$content), 100) }) test_that("/range", { url <- httpbin$url("/range/100") # HEAD handle <- curl::new_handle() curl::handle_setopt(handle, customrequest = "HEAD") resp <- curl::curl_fetch_memory(handle = handle, url) headers <- curl:::parse_headers_list(resp$headers) expect_equal(resp$status_code, 200L) expect_equal(headers$`accept-ranges`, "bytes") expect_equal(headers$`etag`, "range100") abc <- function(n) { l <- paste(letters, collapse = "") substr(strrep(l, n / nchar(l) + 1), 1, n) } # No range header handle <- curl::new_handle() resp <- curl::curl_fetch_memory(handle = handle, url) headers <- curl:::parse_headers_list(resp$headers) expect_equal(resp$status_code, 200L) expect_equal(headers$`accept-ranges`, "bytes") expect_equal(headers$`etag`, "range100") expect_equal(resp$content, charToRaw(abc(100))) # Illegal range headers handle <- curl::new_handle() curl::handle_setheaders(handle, Range = "bytes=20-30, 25-35") resp <- curl::curl_fetch_memory(handle = handle, url) headers <- curl:::parse_headers_list(resp$headers) expect_equal(resp$status_code, 200L) expect_equal(headers$`accept-ranges`, "bytes") expect_equal(headers$`etag`, "range100") expect_equal(resp$content, charToRaw(abc(100))) # Legal header handle <- curl::new_handle() curl::handle_setheaders(handle, Range = "bytes=20-29") resp <- curl::curl_fetch_memory(handle = handle, url) headers <- curl:::parse_headers_list(resp$headers) expect_equal(resp$status_code, 206L) expect_equal(headers$`accept-ranges`, "bytes") expect_equal(headers$`etag`, "range100") expect_equal(resp$content, charToRaw(substr(abc(30), 21, 30))) # In pieces, duration is for the full response url <- httpbin$url("/range/100?chunk_size=1&duration=10") handle <- curl::new_handle() curl::handle_setheaders(handle, Range = "bytes=20-29") resp <- curl::curl_fetch_memory(handle = handle, url) headers <- curl:::parse_headers_list(resp$headers) expect_equal(resp$status_code, 206L) expect_equal(headers$`accept-ranges`, "bytes") expect_equal(headers$`etag`, "range100") expect_equal(resp$content, charToRaw(substr(abc(30), 21, 30))) expect_true(resp$times[["total"]] > 0.5) }) test_that("/uuid", { url <- httpbin$url("/uuid") resp <- curl::curl_fetch_memory(url) echo <- jsonlite::fromJSON(rawToChar(resp$content), simplifyVector = FALSE) expect_match(echo$uuid, "^[-0-9a-z]+$") expect_equal(nchar(echo$uuid), 36) }) test_that("/stream", { url <- httpbin$url("/stream/10") resp <- curl::curl_fetch_memory(url) headers <- curl:::parse_headers_list(resp$headers) expect_equal(resp$status_code, 200) expect_equal(headers[["transfer-encoding"]], "chunked") lines <- strsplit(rawToChar(resp$content), "\n", fixed = TRUE)[[1]] json <- lapply(lines, jsonlite::fromJSON, simplifyVector = TRUE) expect_equal(vapply(json, "[[", integer(1), "id"), 0:9) }) # Cookies ============================================================== test_that("/cookies", { url <- httpbin$url("/cookies") handle <- curl::new_handle() curl::handle_setheaders(handle, Cookie = "foo=bar; bar=baz") resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 200L) data <- jsonlite::fromJSON(rawToChar(resp$content), simplifyVector = TRUE) expect_equal(data, list(cookies = list(foo = "bar", bar = "baz"))) }) test_that("/cookies/set/:name/:value", { url <- httpbin$url("/cookies/set/foo/bar") handle <- curl::new_handle() resp <- curl::curl_fetch_memory(url, handle = handle) headers <- curl::parse_headers(resp$headers, multiple = TRUE) expect_true("HTTP/1.1 302 Found" %in% headers[[1]]) expect_true("Set-Cookie: foo=bar; Path=/" %in% headers[[1]]) expect_true("HTTP/1.1 200 OK" %in% headers[[2]]) expect_equal(resp$status_code, 200L) data <- jsonlite::fromJSON(rawToChar(resp$content), simplifyVector = TRUE) expect_equal(data, list(cookies = list(foo = "bar"))) expect_snapshot(curl::handle_cookies(handle), variant = r_variant()) }) test_that("/cookies/set", { url <- httpbin$url("/cookies/set?foo=bar&bar=baz") handle <- curl::new_handle() resp <- curl::curl_fetch_memory(url, handle = handle) headers <- curl::parse_headers(resp$headers, multiple = TRUE) expect_true("HTTP/1.1 302 Found" %in% headers[[1]]) expect_true("Set-Cookie: foo=bar; Path=/" %in% headers[[1]]) expect_true("Set-Cookie: bar=baz; Path=/" %in% headers[[1]]) expect_true("HTTP/1.1 200 OK" %in% headers[[2]]) expect_equal(resp$status_code, 200L) data <- jsonlite::fromJSON(rawToChar(resp$content), simplifyVector = TRUE) expect_equal(data, list(cookies = list(bar="baz", foo = "bar"))) expect_snapshot(curl::handle_cookies(handle), variant = r_variant()) }) test_that("/cookies/delete", { handle <- curl::new_handle() url1 <- httpbin$url("/cookies/set?foo=bar&bar=baz") resp1 <- curl::curl_fetch_memory(url1, handle = handle) url <- httpbin$url("/cookies/delete?foo") resp <- curl::curl_fetch_memory(url, handle = handle) headers <- curl::parse_headers(resp$headers, multiple = TRUE) expect_true("HTTP/1.1 302 Found" %in% headers[[1]]) expect_true("Set-Cookie: foo=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=0; Path=/" %in% headers[[1]]) expect_true("HTTP/1.1 200 OK" %in% headers[[2]]) expect_equal(resp$status_code, 200L) data <- jsonlite::fromJSON(rawToChar(resp$content), simplifyVector = TRUE) expect_equal(data, list(cookies = list(bar = "baz"))) expect_snapshot(curl::handle_cookies(handle), variant = r_variant()) }) # Images =============================================================== test_that("/image", { url <- httpbin$url("/image") types <- c("image/webp", "image/svg+xml", "image/jpeg", "image/png") for (type in types) { handle <- curl::new_handle() curl::handle_setheaders(handle, accept = type) resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 200L) expect_equal(resp$type, type) } handle <- curl::new_handle() curl::handle_setheaders(handle, accept = "image/*") resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 200L) expect_equal(resp$type, "image/png") handle <- curl::new_handle() curl::handle_setheaders(handle, accept = "application/json") resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 406L) exts <- c("jpeg", "png", "svg", "webp") for (ext in exts) { handle <- curl::new_handle() url <- httpbin$url(paste0("/image/", ext)) resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 200L) expect_equal(resp$type, unname(mime_find(ext))) } }) # Redirects ============================================================ test_that("absolute-redirect", { url <- httpbin$url("/absolute-redirect/4") handle <- curl::new_handle() curl::handle_setopt(handle, followlocation = FALSE) resp <- curl::curl_fetch_memory(url, handle = handle) headers <- curl::parse_headers_list(resp$headers) expect_equal(resp$status_code, 302L) expect_equal(headers$location, httpbin$url("/absolute-redirect/3")) url <- httpbin$url("/absolute-redirect/2") handle <- curl::new_handle() curl::handle_setopt(handle, followlocation = TRUE) resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 200L) expect_equal(resp$url, httpbin$url("/get")) url <- httpbin$url("/absolute-redirect/foo") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 404L) }) test_that("redirect", { url <- httpbin$url("/redirect/4") handle <- curl::new_handle() curl::handle_setopt(handle, followlocation = FALSE) resp <- curl::curl_fetch_memory(url, handle = handle) headers <- curl::parse_headers_list(resp$headers) expect_equal(resp$status_code, 302L) expect_equal(headers$location, "/redirect/3") url <- httpbin$url("/redirect/2") handle <- curl::new_handle() curl::handle_setopt(handle, followlocation = TRUE) resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 200L) expect_equal(resp$url, httpbin$url("/get")) url <- httpbin$url("/redirect/foo") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 404L) }) test_that("/redirect-to", { # default status is 302 handle <- curl::new_handle() curl::handle_setopt(handle, followlocation = FALSE) url <- httpbin$url("/redirect-to", query = list(url = "/get")) resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 302L) expect_equal(resp$type, "text/plain") expect_equal(curl::parse_headers_list(resp$headers)$location, "/get") expect_equal(rawToChar(resp$content), "302 Found. Redirecting to /get") # can specify status handle <- curl::new_handle() curl::handle_setopt(handle, followlocation = FALSE) url <- httpbin$url( "/redirect-to", query = list(url = "/status/200", status_code = 301) ) resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 301L) expect_equal(resp$type, "text/plain") expect_equal(curl::parse_headers_list(resp$headers)$location, "/status/200") expect_equal( rawToChar(resp$content), "301 Moved Permanently. Redirecting to /status/200" ) }) # Anything ============================================================= webfakes/tests/testthat/test-git-app.R0000644000176200001440000000061714740243712017514 0ustar liggesuserstest_that("git_app", { skip_on_cran() dir.create(tmp <- tempfile()) on.exit(unlink(tmp, recursive = TRUE), add = TRUE) untar(testthat::test_path("fixtures/git-repo.tar.gz"), exdir = tmp) app <- git_app(file.path(tmp, "repo")) git <- webfakes::local_app_process(app) expect_snapshot( system( paste("git ls-remote", git$url("/pak-test.git")), intern = TRUE ) ) }) webfakes/tests/testthat/test-http-methods.R0000644000176200001440000000336614172041777020605 0ustar liggesusers test_that("get", { url <- httpbin$url("/get") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 200L) data <- jsonlite::fromJSON(rawToChar(resp$content), simplifyVector = FALSE) expect_equal(data$path, "/get") }) test_that("post", { url <- httpbin$url("/post") data <- charToRaw(jsonlite::toJSON(list(foo = "bar", foobar = 1:3))) handle <- curl::new_handle() curl::handle_setheaders(handle, "Content-Type" = "application/json") curl::handle_setopt( handle, customrequest = "POST", postfieldsize = length(data), postfields = data ) resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 200L) data <- jsonlite::fromJSON(rawToChar(resp$content), simplifyVector = TRUE) expect_equal( data$json, list(foo = "bar", foobar = 1:3) ) expect_equal(data$path, "/post") }) test_methods <- c("connect", "delete", "head", "mkcol", "options", "patch", "propfind", "put", "report") app <- new_app() handler <- function(req, res) res$send_json(list(method = req$method)) for (method in test_methods) app[[method]](paste0("/", method), handler) web2 <- local_app_process(app, port = NA) test_that("the rest", { for (method in test_methods) { url <- web2$url(paste0("/", method)) handle <- curl::new_handle() curl::handle_setheaders(handle, "content-type" = "application/json") curl::handle_setopt(handle, customrequest = toupper(method)) resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status, 200) if (method == "head") { expect_equal(length(resp$content), 0) } else { echo <- jsonlite::fromJSON(rawToChar(resp$content), simplifyVector = TRUE) expect_equal(echo$method, method) } } }) webfakes/tests/testthat/test-tmpl-glue.R0000644000176200001440000000076514172041777020073 0ustar liggesusers app <- new_app() app$engine("html", tmpl_glue()) app$set_config("views", test_path("fixtures", "views")) app$get("/hello/:user", function(req, res) { locals <- c(as.list(req$params), greeting = "hello") html <- res$render("test-view", locals = locals) res$ set_type("text/html")$ send(html) }) web <- local_app_process(app) test_that("glue templating", { url <- web$url("/hello/gabor") resp <- curl::curl_fetch_memory(url) expect_match(rawToChar(resp$content), "hello gabor") }) webfakes/tests/testthat/helper.R0000644000176200001440000000434414740243712016456 0ustar liggesusers test_response_app <- function() { app <- new_app() `%||%` <- function(l, r) if (is.null(l)) r else l app$locals$applocal <- "foo" app$engine("txt", tmpl_glue()) app$get("/local", function(req, res) { res$locals$reslocal <- "bar" "next" }) app$get("/local", function(req, res) { res$send(paste(res$locals$applocal, res$locals$reslocal)) }) app$get("/badengine", function(req, res) { txt <- res$render("foobar") res$send(txt) }) app$get("/badjson", function(req, res) { res$send_json(1:3, text = "foo") }) app$get("/file", function(req, res) { res$send_file( root = system.file(package = "webfakes"), file.path("examples", "static", "public", "foo", "bar.json") ) }) app$get("/type", function(req, res) { res$set_type("json") res$send("{ \"foo\": 1 }") }) app$get("/write", function(req, res) { res$ write("hello ")$ write("world!") }) app$get("/write-header", function(req, res) { res$ set_header("foo", "bar")$ write("hello ")$ write("world!") }) app$get("/write-wait", function(req, res) { res$locals$turn <- (res$locals$turn %||% 0) + 1L if (res$locals$turn == 1) { res$ set_header("content-length", nchar("hello world!"))$ write("hell")$ delay(0.01) } else if (res$locals$turn == 2) { res$ write("o world")$ delay(0.01) } else { res$send("!") } }) app$get("/send-chunk", function(req, res) { res$locals$turn <- (res$locals$turn %||% 0) + 1L if (res$locals$turn == 1) { res$ set_header("Content-Type", "text/plain")$ send_chunk("first chunk\n")$ delay(0.01) } else if (res$locals$turn == 2) { res$ send_chunk("second chunk\n")$ delay(0.01) } else { res$send_chunk("third and final chunk\n") } }) app$get("/add-header", function(req, res) { res$add_header("foo", "bar") res$add_header("foo", "bar2") res$add_header("foobar", "baz") res$send("ready") }) app } httpbin <- local_app_process(httpbin_app()) httpbin$local_env(c(FOO = "{url}xxx")) r_variant <- function() { if (getRversion() < "4.2.0") { "old-r" } else { "new-r" } } webfakes/tests/testthat/test-mw-etag.R0000644000176200001440000000252014172041777017515 0ustar liggesusers app <- new_app()$ use(mw_etag())$ get("/txt", function(req, res) { res$ set_type("text/plain")$ send("textual") })$ get("/txt-empty", function(req, res) { res$ set_type("text/plain")$ send("") })$ get("/raw", function(req, res) { res$ set_type("applicartion/octet-stream")$ send(charToRaw("textual")) })$ get("/raw-empty", function(req, res) { res$ set_type("applicartion/octet-stream")$ send(raw(0)) }) web <- local_app_process(app) test_that("text/plain response", { url <- web$url("/txt") resp <- curl::curl_fetch_memory(url) headers <- curl::parse_headers_list(resp$headers) expect_true("etag" %in% names(headers)) expect_match(headers$etag, "\"[-a-zA-Z0-9]+\"") }) test_that("raw response", { txt <- curl::curl_fetch_memory(web$url("/txt")) raw <- curl::curl_fetch_memory(web$url("/raw")) htxt <- curl::parse_headers_list(txt$headers) hraw <- curl::parse_headers_list(raw$headers) expect_equal(htxt$etag, hraw$etag) }) test_that("etag for empty response", { txt <- curl::curl_fetch_memory(web$url("/txt-empty")) raw <- curl::curl_fetch_memory(web$url("/raw-empty")) htxt <- curl::parse_headers_list(txt$headers) hraw <- curl::parse_headers_list(raw$headers) expect_equal(htxt$etag, "\"00000000\"") expect_equal(htxt$etag, hraw$etag) }) webfakes/tests/testthat/test-mw-log.R0000644000176200001440000000255014172041777017361 0ustar liggesusers app <- new_app()$ use(mw_log())$ get("/txt", function(req, res) { res$ set_type("text/plain")$ send("textual") })$ get("/html", function(req, res) { res$ set_type("text/html")$ send("hello") })$ get("/raw", function(req, res) { res$ set_type("applicartion/octet-stream")$ send(charToRaw("raw")) }) web <- local_app_process(app, callr_opts = list(stdout = "|")) test_that("text/plain response", { url <- web$url("/txt") resp <- curl::curl_fetch_memory(url) plr <- web$.process$poll_io(1000) expect_equal(plr[["output"]], "ready") log <- web$.process$read_output_lines() expect_match(log, "GET http://127\\.0\\.0\\.1:[0-9]*/txt 200 [0-9]+ ms - 7") }) test_that("text/html response", { url <- web$url("/html") resp <- curl::curl_fetch_memory(url) plr <- web$.process$poll_io(1000) expect_equal(plr[["output"]], "ready") log <- web$.process$read_output_lines() expect_match(log, "GET http://127\\.0\\.0\\.1:[0-9]+/html 200 [0-9]+ ms - 44") }) test_that("application/octet-stream response", { url <- web$url("/raw") resp <- curl::curl_fetch_memory(url) plr <- web$.process$poll_io(1000) expect_equal(plr[["output"]], "ready") log <- web$.process$read_output_lines() expect_match(log, "GET http://127\\.0\\.0\\.1:[0-9]+/raw 200 [0-9]+ ms - 3") }) webfakes/tests/testthat/test-path-matching.R0000644000176200001440000000560614172041777020710 0ustar liggesusers test_that("middleware", { expect_true(path_match("foobar", "foobar2", list(method = "use"))) }) test_that("string", { good <- list( list("/foo", "/foo"), list("/", "/"), list("/***", "/***") ) for (x in good) { expect_true(path_match("get", x[[1]], list(method = "get", path = x[[2]]))) } bad <- list( list("/foo", "/bar"), list("/foo", "foo") ) for (x in bad) { expect_false(path_match("get", x[[1]], list(method = "get", path = x[[2]]))) } }) test_that("character vector or list", { good <- list( list("/foo", c("/foo2", "/foo")), list("/", list("notthis", "/")) ) for (x in good) { expect_true(path_match("get", x[[1]], list(method = "get", path = x[[2]]))) } bad <- list( list("/foo", list()) ) for (x in bad) { expect_false(path_match("get", x[[1]], list(method = "get", path = x[[2]]))) } }) named_list <- function() structure(list(), names = character()) test_that("regexp", { good <- list( list("/foo", new_regexp("^/fo+$"), list(params = named_list())), list("/", list(new_regexp("/foobar"), new_regexp("^/$")), list(params = named_list())) ) for (x in good) { expect_equal( path_match("get", x[[1]], list(method = "get", path = x[[2]])), x[[3]] ) } bad <- list( list("/foo", list("/foobar")) ) for (x in bad) { expect_false(path_match("get", x[[1]], list(method = "get", path = x[[2]]))) } }) test_that("list of things", { good <- list( list("/foo", list("/foo2", new_regexp("/foo")), list(params = named_list())), list("/", list(new_regexp("notthis"), "/"), TRUE) ) for (x in good) { expect_equal( path_match("get", x[[1]], list(method = "get", path = x[[2]])), x[[3]] ) } bad <- list( list("/foo", list()) ) for (x in bad) { expect_false(path_match("get", x[[1]], list(method = "get", path = x[[2]]))) } }) test_that("regexp with capture groups", { good <- list( list( "/foo/bar", new_regexp("^/foo/([a-z]+)/?$"), list(params = structure(list("bar"), names = "")) ), list( "/foo/bar", new_regexp("^/(?f.*)/(?[a-z]+)/?$"), list(params = structure(list("foo", "bar"), names = c("x", "y"))) ) ) for (x in good) { expect_equal( path_match("get", x[[1]], list(method = "get", path = x[[2]])), x[[3]] ) } bad <- list( list("/foo/bar", new_regexp("/foox/([a-z]+)/?$")) ) for (x in bad) { expect_false(path_match("get", x[[1]], list(method = "get", path = x[[2]]))) } }) test_that("tokens", { good <- list( list( "/foo", "/:x", list(params = list(x = "foo")) ), list( "/foo/bar", "/:x/:y", list(params = list(x = "foo", y = "bar")) ) ) for (x in good) { expect_equal( path_match("get", x[[1]], list(method = "get", path = x[[2]])), x[[3]] ) } }) webfakes/tests/testthat/test-mw-static.R0000644000176200001440000000474214310055034020055 0ustar liggesusers app <- new_app() app$use(mw_etag()) app$use(mw_static(root = test_path("fixtures", "static"))) set_headers <- function(req, res) { res$set_header("foo", "bar") } app$use(mw_static(root = test_path("fixtures", "static2"), set_headers = set_headers)) app$get("/static.html", function(req, res) { res$send("this is never reached") }) app$get("/fallback", function(req, res) { res$send("this is the fallback") }) web <- local_app_process(app) test_that("static file", { url <- web$url("/static.html") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 200L) # type is set from the file extension expect_equal(resp$type, "text/html") expect_equal( rawToChar(resp$content), read_char(test_path("fixtures", "static", "static.html")) ) }) test_that("static file in subdirectory", { url <- web$url("/subdir/static.json") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 200L) # type is set from the file extension expect_equal(resp$type, "application/json") expect_equal( rawToChar(resp$content), read_char(test_path("fixtures", "static", "subdir", "static.json")) ) }) test_that("file not found", { url <- web$url("/notfound") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 404L) }) test_that("file not found falls back", { url <- web$url("/fallback") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 200L) expect_equal(resp$type, "text/plain") expect_equal( rawToChar(resp$content), "this is the fallback" ) }) test_that("directory is 404", { url <- web$url("/subdir") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 404L) }) test_that("set_headers callback", { url <- web$url("/static.tar.gz") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 200L) expect_equal(resp$type, "application/gzip") headers <- curl::parse_headers_list(resp$headers) expect_equal(headers$foo, "bar") expect_equal( resp$content, read_bin(test_path("fixtures", "static2", "static.tar.gz")) ) }) test_that("if-none-match is respected", { url <- web$url("/static.html") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 200L) etag <- curl::parse_headers_list(resp$headers)$etag h <- curl::new_handle() curl::handle_setheaders(h, "If-None-Match" = etag) resp2 <- curl::curl_fetch_memory(url, handle = h) expect_equal(resp2$status_code, 304L) expect_equal(resp2$content, raw(0)) }) webfakes/tests/testthat/test-uuid.R0000644000176200001440000000110014172041777017113 0ustar liggesusers test_that("uuid_random() format", { uu <- replicate(1000, uuid_random()) # format is right regex <- paste0( "^[0-9a-f]{8}-", "[0-9a-f]{4}-", "4[0-9a-f]{3}-", "[89ab][0-9a-f]{3}-", "[0-9a-f]{12}$" ) expect_true(all(grepl(regex, uu, perl = TRUE))) # all bits are used, except for the dashes and M and N pos <- setdiff(1:36, c(9, 14, 19, 24, 15, 20)) xd <- format(as.hexmode(0:15)) for (p in pos) expect_true(all(xd %in% substr(uu, p, p))) # all bits are used for N expect_true(all(c("8", "9", "a", "b") %in% substr(uu, 20, 20))) }) webfakes/tests/testthat/test-decode-url.R0000644000176200001440000000171514740243712020176 0ustar liggesusers test_that("decode_url option", { app <- webfakes::new_app() app$get( webfakes::new_regexp("^/hello/(?.*)$"), function(req, res) { res$send(paste0("Return content of ", req$params$path, "!")) } ) web <- webfakes::local_app_process(app) resp <- curl::curl_fetch_memory(web$url("/hello/foo%2fbar/suffix")) expect_equal(resp$status_code, 200L) expect_equal( rawToChar(resp$content), "Return content of foo/bar/suffix!" ) app <- webfakes::new_app() app$get( webfakes::new_regexp("^/hello/(?.*)$"), function(req, res) { res$send(paste0("Return content of ", req$params$path, "!")) } ) web <- webfakes::local_app_process( app, opts = server_opts(remote = TRUE, decode_url = FALSE) ) resp <- curl::curl_fetch_memory(web$url("/hello/foo%2fbar/suffix")) expect_equal(resp$status_code, 200L) expect_equal( rawToChar(resp$content), "Return content of foo%2fbar/suffix!" ) }) webfakes/tests/testthat/test-local.R0000644000176200001440000000046614172041777017255 0ustar liggesusers test_that("app from setup", { # This app was created in setup.R url <- httpbin2$url("/get", query = c(q1 = "one", q2 = "two")) handle <- curl::new_handle() curl::handle_setheaders(handle, "foo" = "bar") resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 200L) }) webfakes/tests/testthat/test-mw-urlencoded.R0000644000176200001440000000141714172041777020725 0ustar liggesusers app <- new_app() app$use(mw_urlencoded()) app$post("/form", function(req, res) { ret <- list( form = req$form ) res$send_json(ret, pretty = TRUE, auto_unbox = TRUE) }) web <- local_app_process(app) test_that("mw-urlencoded", { url <- web$url("/form") handle <- curl::new_handle() data <- charToRaw("foo=bar&foobar=100") curl::handle_setheaders( handle, "content-type" = "application/x-www-form-urlencoded" ) curl::handle_setopt( handle, customrequest = "POST", postfieldsize = length(data), postfields = data ) resp <- curl::curl_fetch_memory(url, handle = handle) echo <- jsonlite::fromJSON( rawToChar(resp$content), simplifyVector = FALSE ) expect_equal(echo, list(form = list("foo" = "bar", "foobar" = "100"))) }) webfakes/tests/testthat/_snaps/0000755000176200001440000000000014740243712016332 5ustar liggesuserswebfakes/tests/testthat/_snaps/git-app.md0000644000176200001440000000061614740246123020217 0ustar liggesusers# git_app Code system(paste("git ls-remote", git$url("/pak-test.git")), intern = TRUE) Output [1] "3f3b0b4ee8a0ff4563073924e5fe069da67a6d8b\tHEAD" [2] "3f3b0b4ee8a0ff4563073924e5fe069da67a6d8b\trefs/heads/main" [3] "cefdc0eebcd7f757efb9a80652fd8aaf1a87508e\trefs/heads/subdir" [4] "cefdc0eebcd7f757efb9a80652fd8aaf1a87508e\trefs/tags/v1" webfakes/tests/testthat/_snaps/app-process.md0000644000176200001440000000105614740246112021107 0ustar liggesusers# error if cannot start Code new_app_process(app, process_timeout = 100, start = TRUE) Condition Error in `self$start()`: ! webfakes app subprocess did not start :( --- Code new_app_process(app, start = TRUE) Condition Error: ! failed to start webfakes app process: in callr subprocess. Caused by error: ! oops --- Code new_app_process(app, start = TRUE) Condition Error in `self$start()`: ! Unexpected message from webfakes app subprocess. Report a bug please. webfakes/tests/testthat/_snaps/old-r/0000755000176200001440000000000014740243712017347 5ustar liggesuserswebfakes/tests/testthat/_snaps/old-r/httpbin.md0000644000176200001440000000117614740243712021346 0ustar liggesusers# /cookies/set/:name/:value Code curl::handle_cookies(handle) Output domain flag path secure expiration name value 1 127.0.0.1 FALSE / FALSE foo bar # /cookies/set Code curl::handle_cookies(handle) Output domain flag path secure expiration name value 1 127.0.0.1 FALSE / FALSE foo bar 2 127.0.0.1 FALSE / FALSE bar baz # /cookies/delete Code curl::handle_cookies(handle) Output domain flag path secure expiration name value 1 127.0.0.1 FALSE / FALSE bar baz webfakes/tests/testthat/_snaps/request.md0000644000176200001440000000062014740246162020344 0ustar liggesusers# parse_query Code parse_query("foo") Output $foo [1] "" Code parse_query("?foo") Output $foo [1] "" Code parse_query("?foo&bar") Output $foo [1] "" $bar [1] "" Code parse_query("?foo&bar=baz") Output $foo [1] "" $bar [1] "baz" webfakes/tests/testthat/_snaps/new-r/0000755000176200001440000000000014740243712017362 5ustar liggesuserswebfakes/tests/testthat/_snaps/new-r/httpbin.md0000644000176200001440000000117614740246130021356 0ustar liggesusers# /cookies/set/:name/:value Code curl::handle_cookies(handle) Output domain flag path secure expiration name value 1 127.0.0.1 FALSE / FALSE Inf foo bar # /cookies/set Code curl::handle_cookies(handle) Output domain flag path secure expiration name value 1 127.0.0.1 FALSE / FALSE Inf foo bar 2 127.0.0.1 FALSE / FALSE Inf bar baz # /cookies/delete Code curl::handle_cookies(handle) Output domain flag path secure expiration name value 1 127.0.0.1 FALSE / FALSE Inf bar baz webfakes/tests/testthat/_snaps/mw-range-parser.md0000644000176200001440000000145314740246137021672 0ustar liggesusers# parse_range Code parse_range("foobar=1-100") Output NULL Code parse_range("bytes=0-100, 50-150") Output NULL Code parse_range("bytes=200-100") Output NULL Code parse_range("bytes=x-100") Output NULL Code parse_range("bytes=1-100") Output [,1] [,2] [1,] 1 100 Code parse_range("bytes=1-") Output [,1] [,2] [1,] 1 Inf Code parse_range("bytes=-100") Output [,1] [,2] [1,] 0 -100 Code parse_range("bytes=0-100, 200-") Output [,1] [,2] [1,] 0 100 [2,] 200 Inf Code parse_range("bytes=200-300, 0-100") Output [,1] [,2] [1,] 0 100 [2,] 200 300 webfakes/tests/testthat/test-request.R0000644000176200001440000000024314740243712017636 0ustar liggesusers test_that("parse_query", { expect_snapshot({ parse_query("foo") parse_query("?foo") parse_query("?foo&bar") parse_query("?foo&bar=baz") }) }) webfakes/tests/testthat/test-response.R0000644000176200001440000000462414172041777020021 0ustar liggesusers web <- local_app_process(test_response_app()) test_that("response locals", { url <- web$url("/local") resp <- curl::curl_fetch_memory(url) expect_equal(rawToChar(resp$content), "foo bar") }) # on_response is tested via mw_log() test_that("render", { # if the template or engine does not exist url <- web$url("/badengine") resp <- curl::curl_fetch_memory(url) expect_match( rawToChar(resp$content), "Cannot find template engine for view" ) }) test_that("send_json", { url <- web$url("/badjson") resp <- curl::curl_fetch_memory(url) expect_match( rawToChar(resp$content), "Specify only one of" ) }) test_that("send_file", { url <- web$url("/file") resp <- curl::curl_fetch_memory(url) path <- system.file( package = "webfakes", "examples", "static", "public", "foo", "bar.json" ) expect_equal(resp$content, read_bin(path)) }) test_that("set_type", { url <- web$url("/type") resp <- curl::curl_fetch_memory(url) headers <- curl::parse_headers_list(resp$headers) expect_equal(headers$`content-type`, "application/json") }) test_that("write", { url <- web$url("/write") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 200) expect_equal(rawToChar(resp$content), "hello world!") # header can be set url <- web$url("/write-header") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 200) expect_equal(rawToChar(resp$content), "hello world!") headers <- curl::parse_headers_list(resp$headers) expect_equal(headers$`foo`, "bar") }) test_that("write-wait", { url <- web$url("/write-wait") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 200) expect_equal(rawToChar(resp$content), "hello world!") }) test_that("send_chunk", { url <- web$url("/send-chunk") resp <- curl::curl_fetch_memory(url) expect_equal(resp$status_code, 200) expect_equal( rawToChar(resp$content), "first chunk\nsecond chunk\nthird and final chunk\n" ) headers <- curl::parse_headers_list(resp$headers) expect_equal(headers[["content-type"]], "text/plain") expect_equal(headers[["transfer-encoding"]], "chunked") }) test_that("add_header", { url <- web$url("/add-header") resp <- curl::curl_fetch_memory(url) headers <- curl::parse_headers_list(resp$headers) expect_equal( headers[names(headers) == "foo"], list(foo = "bar", foo = "bar2") ) expect_equal(headers[["foobar"]], "baz") }) webfakes/tests/testthat/test-base64.R0000644000176200001440000000103614172041777017241 0ustar liggesusers test_that("base64_decode", { decode_tests <- c( 'YWE=' = 'aa', ' YWE=' = 'aa', 'Y WE=' = 'aa', 'YWE= ' = 'aa', "Y\nW\r\nE=" = 'aa', 'YWE=====' = 'aa', # extra padding 'YWE' = 'aa', # missing padding 'YWFh====' = 'aaa', 'YQ' = 'a', 'Y' = '', 'x==' = '' ) for (i in seq_along(decode_tests)) { encoded <- names(decode_tests)[[i]] expected <- decode_tests[[i]] decoded <- base64_decode(encoded) expect_equal(decoded, expected) } }) webfakes/tests/testthat/test-delay.R0000644000176200001440000000204414172041777017253 0ustar liggesusers app <- new_app() app$get("/delay", function(req, res) { if (is.null(res$locals$seen)) { res$locals$seen <- TRUE res$delay(.5) } else { res$send_json( list(message = "Sorry, running late..."), auto_unbox = TRUE ) } }) app$get("/nodelay", function(req, res) { res$send_json( list(message = "I am fast, aren't I?"), auto_unbox = TRUE ) }) web <- local_app_process(app, opts = server_opts(num_threads = 2)) test_that("delay", { p <- curl::new_pool(multiplex = FALSE) h1 <- curl::new_handle(url = web$url("/delay")) h2 <- curl::new_handle(url = web$url("/nodelay")) resp1 <- resp2 <- NULL curl::multi_add(h1, done = function(x) resp1 <<- x, fail = stop, pool = p) curl::multi_add(h2, done = function(x) resp2 <<- x, fail = stop, pool = p) curl::multi_run(timeout = 2, pool = p) curl::multi_cancel(h1) curl::multi_cancel(h2) expect_false(is.null(resp1)) expect_false(is.null(resp2)) expect_true(resp1$times[["total"]] > 0.5) expect_true(resp2$times[["total"]] < resp1$times[["total"]]) }) webfakes/tests/testthat/test-app.R0000644000176200001440000000031714172041777016736 0ustar liggesusers test_that("invalid handler", { app <- new_app() expect_error( app$use("foobar"), "Invalid webfakes handler" ) expect_error( app$get("/foo", 1:100), "Invalid webfakes handler" ) }) webfakes/tests/testthat/test-mw-raw.R0000644000176200001440000000215314172041777017370 0ustar liggesusers app <- new_app() app$use(mw_raw()) app$post("/raw", function(req, res) { res$ set_type("application/octet-stream")$ send(req$raw) }) web <- local_app_process(app) test_that("raw body parser", { url <- web$url("/raw") data <- charToRaw(jsonlite::toJSON(list(foo = "bar", foobar = 1:3))) handle <- curl::new_handle() curl::handle_setheaders(handle, "content-type" = "application/octet-stream") curl::handle_setopt( handle, customrequest = "POST", postfieldsize = length(data), postfields = data ) resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 200L) expect_equal(data, resp$content) }) test_that("non-matching content-type", { url <- web$url("/raw") data <- charToRaw(jsonlite::toJSON(list(foo = "bar", foobar = 1:3))) handle <- curl::new_handle() curl::handle_setheaders(handle, "content-type" = "application/json") curl::handle_setopt( handle, customrequest = "POST", postfieldsize = length(data), postfields = data ) resp <- curl::curl_fetch_memory(url, handle = handle) expect_equal(resp$status_code, 404L) }) webfakes/tests/testthat/setup.R0000644000176200001440000000013514172041777016337 0ustar liggesusers httpbin2 <- local_app_process( httpbin_app(), .local_envir = testthat::teardown_env() ) webfakes/tests/testthat.R0000644000176200001440000000012314740243712015166 0ustar liggesuserslibrary(testthat) library(webfakes) test_check("webfakes", reporter = "progress") webfakes/MD50000644000176200001440000002227014740436161012362 0ustar liggesusersac70725ffa5e8bcee575b55cdf1bbc36 *DESCRIPTION c0288025d92db56102e8471b0a7f6a9a *LICENSE de4abb60cda1343e55e18ed08d50161e *NAMESPACE 04b2242671f1c824c0eaf54d509b08fd *NEWS.md cbd7632589cb5e1f437d4886d8700d14 *R/app-process.R 0641d5cd989ef14b3dfc8ea7c8d39849 *R/app.R d8dbb0089c19547d728fd8f9cf8bf41c *R/base64.R a15ee3dddecc6fe3faaf206de2e61591 *R/cleancall.R 74da295dd7a1e9db2b9a6e746cf9ae4a *R/compat-defer.R cc4d7116b0e1f49b56ecde3342e71c12 *R/digest.R 50c1b69565d3fa0b3775788a0bd29938 *R/docs.R 0222083612da5ba40030fcb1db784d68 *R/git-app.R 974f0511d73c2eb41187750b955b9c34 *R/httpbin.R 319f8c63839322c1d503fad9c9291a6d *R/local-app-process.R 75ecdf4307a22a8de5e067e4e29f9ee2 *R/mime.R e824853dd98e8cf2e0b90f04ecbf1036 *R/mw-authorization.R 89f28abaf203e4ad53a0c55f462ff4a2 *R/mw-cgi.R 024be56b2b76dfb774b4c900ab8621c0 *R/mw-cookie-parser.R 7146245e21f33165284c73a7693f5476 *R/mw-etag.R 16703673e4118b67ec1b83f6c869c4fd *R/mw-json.R 0c9dd51fee33b3f0425aaf4cef846fa3 *R/mw-log.R 016fc22ce24383234d77c649b00e5d7f *R/mw-multipart.R c1a2c006e911cdfb50dfee47d2f26d1d *R/mw-range-parser.R b42c2679443555f47707a64edb108739 *R/mw-raw.R d0b8c70f6723bfc088235a6ca14bf37c *R/mw-static.R 1cc38594e39b091d2a6ab87a44983929 *R/mw-text.R 56fda89046f966a7ff2bfac92ef2fb98 *R/mw-urlencoded.R 35e3708952cba1712724a6fd11c44789 *R/oauth.R c2b7311ad5106049d6440ef69857d968 *R/package.R 635cdfce5501b303073ecc5be51000a8 *R/path.R 3257054aef08ca58755e7b134f54e1d7 *R/print.R 260e9f54b6f663b907494ad53dd14d53 *R/rematch2.R 5ce5bfeb2c2e3946fe2cd4b8706e4bdb *R/request.R 905436bc14dfc849cdc140b771588d97 *R/response.R 48eff9c3934d822389f3379ac2e35259 *R/server.R d8b172f2ffc307fb39342b869b3bda25 *R/status.R 0841cfdaedaa0a976afc73f77d4cb9ba *R/tmpl-glue.R 46a5ebc593ce5bed7caba42c5ad1b488 *R/utils.R 4aca6f8e0b573be0301335a8be038814 *R/uuid.R df6cc46bc7fae1a55b713f3d5065b35a *R/webfakes-package.R 072e11690209746a59e6c0ba9a9c6f94 *README.md d3943445ebf976356df3874cc85c2f43 *inst/credits/ciwetweb.md bb9faae548cd71cee0803bd99f1df5da *inst/credits/redoc.md 6c5a82320bfdd143bf45cee7253e53ab *inst/examples/hello/app.R c5a7448fdb6c04af06978c875f979b84 *inst/examples/hello/views/test.txt 05d41d4c0a7e1c6260b3f2c0f5eccba3 *inst/examples/httpbin/app.R a3cd10015af1713b5d09017677552f51 *inst/examples/httpbin/assets/forms-post.html 548ccf782a4c899f10512a8609cda28e *inst/examples/httpbin/assets/httpbin.html d439215e6d34774c286caeaa5dddb2d0 *inst/examples/httpbin/data/deny.txt 49b221c274d8d1545e577538278e25f9 *inst/examples/httpbin/data/example.html 302c1d78011b9926fcf3e6a5e556e7a1 *inst/examples/httpbin/data/example.json a88c5078ffed61c2fff326de48cdb740 *inst/examples/httpbin/data/example.xml 46fd03688e49c6b6b0a2b7d3553c1e42 *inst/examples/httpbin/data/robots.txt c159c6400f3b55c71740339832e11795 *inst/examples/httpbin/data/utf8.html e1f9ee4cf1f2be1c1d3d824ba25133b4 *inst/examples/httpbin/doc-template.hbs d67bd2c5bff4abba2bd11fb0ab07d404 *inst/examples/httpbin/images/Rlogo.jpeg 7381224c65138a2acdf3a8346f8275c4 *inst/examples/httpbin/images/Rlogo.png 2b5719958c377e203c20463e340671f3 *inst/examples/httpbin/images/Rlogo.svg 1a5760d159f118ba8d495ce2c7e082d7 *inst/examples/httpbin/images/Rlogo.webp 949c4429bb552dea6c3534e9e6e5132f *inst/examples/httpbin/openapi.yaml 7381224c65138a2acdf3a8346f8275c4 *inst/examples/send-file/Rlogo.png 73a1573d18e67d153e6a07a2dc9f4cf9 *inst/examples/send-file/app.R 4bfb13b752f6319afc729dde9e791be3 *inst/examples/static/app.R b264e4c43787cb81e7d4c5eb4b006427 *inst/examples/static/public/bar/foo.txt 5186b0805ea39b8efd70a1d3c85f7a89 *inst/examples/static/public/foo/bar.html 7c2c080a60bdbcf4ebe6de2a4c27f8ac *inst/examples/static/public/foo/bar.json a163d4f17cf2ccba7498236176e9e87e *inst/views/authorize.html 2f092ec4e63649645669691390894dd9 *man/git_app.Rd 5cb19c23320fd60635e35f5bede220c7 *man/glossary.Rd 652a6e30f94da913587b158101070471 *man/http_time_stamp.Rd e02aba10f335675508df8bb61428c8bb *man/httpbin_app.Rd 2f72d34848cd14a4608038f081957a8d *man/local_app_process.Rd a0f6148cbb31e2f899d367ed5c5f0acc *man/mw_cgi.Rd 3aa272e5a02594021c1364746741b6d4 *man/mw_cookie_parser.Rd 537037de324e27bcd83b8d4d8f5c38a7 *man/mw_etag.Rd 9391c960878a639563f83fb7fb3a8b05 *man/mw_json.Rd 603cba68c57fb0982111b0989b40e626 *man/mw_log.Rd b4d013720e940067ce0473ef60cfda89 *man/mw_multipart.Rd 9951893b045f59572b0d03f33067fc24 *man/mw_range_parser.Rd 4997517a159351ce37a12b71324a1adb *man/mw_raw.Rd ff63aa18cb775898ffcc419e96f7c80a *man/mw_static.Rd ea7f0595779b547e3b68cd14ea5e9813 *man/mw_text.Rd f34fcdb907ddd8792c41460b366acfd8 *man/mw_urlencoded.Rd 0b29f830342828ab5a26329c26de24a9 *man/new_app.Rd ec1fd901f0a558aee54054ab40ed1c45 *man/new_app_process.Rd a00ceeb3738efcbcf68322a7c5e3574a *man/new_regexp.Rd 4c1a98cec2cdc8aada531b85a4cced94 *man/oauth2_httr_login.Rd 39313747ae2fd30159e298a56ca9f36d *man/oauth2_login.Rd 2e27ada1a5090d77cc05ed77f78ad232 *man/oauth2_resource_app.Rd e82f9b5f0a5fb5730d5e8ae36c7b563d *man/oauth2_third_party_app.Rd 9a855c74792013bd993bea790d7e8682 *man/rmd-fragments/oauth2.Rmd 1aced1eec508c45604a2558e091a846d *man/server_opts.Rd 3acb0a67e7b026f6517ff82c6020d515 *man/tmpl_glue.Rd 8292052dbb5f1b45d34809fd76c4acb0 *man/webfakes-package.Rd 517c71d331371eb9c1a5ed4ecc52fffd *man/webfakes_request.Rd 2214a9b6c75ec38ed5afc67cb85d0ece *man/webfakes_response.Rd c2f08616d8cd19f81a49645470da7257 *src/Makevars 800d9d50abf3f040b7e12b4591530399 *src/Makevars.win 495f824547bf57f2de2c2ea6ef59e24a *src/civetweb.c a96adb23ce8a048124f001a96e49354f *src/civetweb.h 2db4898abcab6bce2d202f0fdc371fee *src/cleancall.c 3cedbcdfb4d38768d2765d57a891eee1 *src/cleancall.h b935423483e3a08ab1dcc620c752d195 *src/crc32.c 9f1ee6d9b40e7a78246c6b6ffcfdfbc2 *src/errors.c 1ac3a7b49282794e006a1715f6ccf8af *src/errors.h 09c2fab815099e1a898120f6aa44c316 *src/handle_form.h de6cdab982e6dacaff9ea12e0bad607b *src/http2.h 8dab28a3e111536d33d68a56ca33869e *src/match.h 004860b6c1cbf57de18d0a88f34adc19 *src/md5.h 313790b5e6faeb60fc0e7fafe40106f8 *src/mod_mbedtls.h 4c574178c50497bd958db412e8c44ef3 *src/response.h df8c12b6ac0ea7fb58e5eaf3b7961ec4 *src/rweb.c 282f9fcb2cb68ea168890f69fb2289c6 *src/sha1.h 3ceaa481066c12c0c733bb5a1d68d504 *src/sort.h 6232fe0e510463a3c35df3c3f64dffd3 *src/timer.h 6e2ee64196e5ba2eeb232d96f1f6da19 *tests/testthat.R 88f382b4fd4e87309b385a6f4a0cac22 *tests/testthat/_snaps/app-process.md 8dcc806ca4302ee3a8dbac408c2ef01b *tests/testthat/_snaps/git-app.md a830d7ad09a1b2af0bf8245db3f5c04a *tests/testthat/_snaps/mw-range-parser.md c6810546c0f7ec95048df89cca621938 *tests/testthat/_snaps/new-r/httpbin.md 39952e9ac2d30ff27ac6b26ac0fe5f8f *tests/testthat/_snaps/old-r/httpbin.md 7bfb591d347f562480da0a047fd99572 *tests/testthat/_snaps/request.md 63fc9d71fd241752113aca48b7a36039 *tests/testthat/fixtures/git-repo.tar.gz 53eab8f2e4faa72bc61706f266402a3a *tests/testthat/fixtures/output/webfakes_app.txt bc94c73f4a8795d82d11959bb8977080 *tests/testthat/fixtures/output/webfakes_app_process.txt 3eb28c5bbcf1b7012d10f79b344a2359 *tests/testthat/fixtures/output/webfakes_regexp.txt d454a35a5b8574d65605720fefffca34 *tests/testthat/fixtures/output/webfakes_request.txt 6e5e7fe88bfaa3a0a7b1249a330708be *tests/testthat/fixtures/output/webfakes_response.txt 5186b0805ea39b8efd70a1d3c85f7a89 *tests/testthat/fixtures/static/static.html cef8a5c5d7fcfb9cf601bf3a146a47e7 *tests/testthat/fixtures/static/subdir/static.json 5186b0805ea39b8efd70a1d3c85f7a89 *tests/testthat/fixtures/static2/static.html f6b1e97351bf27f84ff66285cfd3e033 *tests/testthat/fixtures/static2/static.tar.gz cef8a5c5d7fcfb9cf601bf3a146a47e7 *tests/testthat/fixtures/static2/subdir/static.json 01d5c74047c909bf938df6556a623e39 *tests/testthat/fixtures/views/test-view.html eb423d3cd675281b37246138b17e4e31 *tests/testthat/helper.R 77ad3614c80d1e54c56297f4391d0806 *tests/testthat/setup.R 7de0af4bf6553d088906014d8831dad6 *tests/testthat/teardown.R 9d8ee96285c56589b8dcad3ec740a5a3 *tests/testthat/test-app-process.R d2f3ba01556b63c3c56f8c069cb065f8 *tests/testthat/test-app.R 8561f7c34ea02be68ee231e1fef2d18a *tests/testthat/test-base64.R f92052259ba828d350025f02fb002ada *tests/testthat/test-decode-url.R a83f70864880a222919197f0e862a8c0 *tests/testthat/test-delay.R 6d602c878020448f07f814c870bb6b64 *tests/testthat/test-git-app.R 4667b6a3c6da1899e02b6ae7565a0580 *tests/testthat/test-http-methods.R eb1cac99d8306125a72aaec116da528c *tests/testthat/test-httpbin.R ddbc916550d7bac609392f6683663b4a *tests/testthat/test-local.R 3298e815bfcc7198a44fc6dc904f8b98 *tests/testthat/test-mime.R 35c1fe0841038047fd8729321ef52dbd *tests/testthat/test-mw-etag.R 2456bc02def9a1516af87492dc866a34 *tests/testthat/test-mw-log.R 8ced3ab7e5b587fadd0e2772c594a422 *tests/testthat/test-mw-multipart.R 777e7c3804e392b08c8b2f13363d4f68 *tests/testthat/test-mw-range-parser.R 0e0abb5c165b3080a5ad110f60add9b1 *tests/testthat/test-mw-raw.R 8158d3200eedf095e95899e96e896d3f *tests/testthat/test-mw-static.R d1a69d7d64107051d4b104494f1e366e *tests/testthat/test-mw-urlencoded.R 206ffde40f21d7fec53b0a7f5981cdb7 *tests/testthat/test-oauth.R 11b6488e4a8debdf26dfd91c5c1f5764 *tests/testthat/test-path-matching.R 2eeca807e2fd0ce0d507d14e406500aa *tests/testthat/test-print.R 075e3ab1331cb2ab395eef5690f16208 *tests/testthat/test-request.R c71c58d265970aa28f16f0dd0d0f9aef *tests/testthat/test-response.R e733460674af97d0794afc89c967599a *tests/testthat/test-tmpl-glue.R f8b065cc5967c5ece35bbff6dd056eaf *tests/testthat/test-uuid.R webfakes/R/0000755000176200001440000000000014740430506012245 5ustar liggesuserswebfakes/R/uuid.R0000644000176200001440000000101114172041777013336 0ustar liggesusers uuid_random <- function() { # These uuids are pseudo-random, they are not secure! # xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx (8-4-4-4-12) # The 4 bits of digit M are the UUID version, # and the 1 to 3 most significant bits of digit N code the UUID variant. digits <- floor(stats::runif(32, 0, 16)) digits[13] <- 0x4 digits[17] <- bitwOr(bitwAnd(digits[17], 0x3), 0x8) x <- format(as.hexmode(digits)) paste( collapse = "", c(x[1:8], "-", x[9:12], "-", x[13:16], "-", x[17:20], "-", x[21:32]) ) } webfakes/R/digest.R0000644000176200001440000000020314172041777013651 0ustar liggesusers crc32 <- function(x) { if (is.character(x)) x <- charToRaw(x) stopifnot(is.raw(x)) call_with_cleanup(c_webfakes_crc32, x) } webfakes/R/mw-range-parser.R0000644000176200001440000000424314740243712015403 0ustar liggesusers #' Middleware to parse a Range header #' #' Adds the requested ranges to the `ranges` element of the request #' object. `request$ranges` is a data frame with two columns, `from` and #' `to`. Each row corresponds one requested interval. #' #' When the last `n` bytes of the file are requested, the matrix row is set #' to `c(0, -n)`. When all bytes after a `p` position are requested, the #' matrix row is set to `c(p, Inf)`. #' #' If the intervals overlap, then `ranges` is not set, i.e. the `Range` #' header is ignored. #' #' If its syntax is invalid or the unit is not `bytes`, then the #' `Range` header is ignored. #' #' @return Handler function. #' #' @family middleware #' @export mw_range_parser <- function() { function(req, res) { rh <- req$get_header("Range") if (length(rh) == 0) return("next") req$ranges <- parse_range(rh) "next" } } parse_range <- function(rh) { rh <- trimws(rh) if (length(rh) == 0 || !startsWith(rh, "bytes=")) return() rh <- sub("^bytes=[ ]*", "", rh) rngs <- trimws(strsplit(rh, ",", fixed = TRUE)[[1]]) res <- matrix(integer(1), nrow = length(rngs), ncol = 2) for (i in seq_along(rngs)) { rng <- strsplit(rngs[i], "-")[[1]] if (length(rng) < 1 || length(rng) > 3) { return() } else if (length(rng) == 1) { res[i, 1] <- parse_int(rng[1]) res[i, 2] <- Inf if (is.na(res[i, 1]) || res[i, 1] < 0) return() } else if (rng[1] == "") { res[i, 1] <- 0 res[i, 2] <- -parse_int(rng[2]) if (is.na(res[i, 2]) || res[i, 2] > 0) return() } else { res[i, 1] <- parse_int(rng[1]) res[i, 2] <- parse_int(rng[2]) if (is.na(res[i, 1]) || is.na(res[i, 2]) || res[i, 1] < 0 || res[i, 2] < 0 || res[i, 1] > res[i, 2]) { return() } } } res <- res[order(res[,1]), , drop = FALSE] # check for overlapping intervals if (intervals_overlap(res)) return() res } parse_int <- function(x) { suppressWarnings(as.integer(x)) } intervals_overlap <- function(x) { # assume that it is sorted on first column # then every interval needs to finish before the next one starts if (nrow(x) <= 1) return(FALSE) any(x[,2][-nrow(x)] >= x[,1][-1]) } webfakes/R/httpbin.R0000644000176200001440000006616114740243712014053 0ustar liggesusers #' Generic web app for testing HTTP clients #' #' A web app similar to `https://httpbin.org`. #' See [its specific docs](https://webfakes.r-lib.org/httpbin.html). #' You can also see these docs locally, by starting the app: #' ```r #' httpbin <- new_app_process(httpbin_app()) #' browseURL(httpbin$url()) #' ``` #' #' @param log Whether to log requests to the standard output. #' @return A `webfakes_app`. #' #' @export #' @examples #' app <- httpbin_app() #' proc <- new_app_process(app) #' url <- proc$url("/get") #' resp <- curl::curl_fetch_memory(url) #' curl::parse_headers_list(resp$headers) #' cat(rawToChar(resp$content)) #' proc$stop() httpbin_app <- function(log = interactive()) { encode_files <- function(files) { for (i in seq_along(files)) { files[[i]]$value <- paste0( "data:application/octet-stream;base64,", base64_encode(files[[i]]$value) ) } files } app <- new_app() # Log requests by default if (log) app$use("logger" = mw_log()) # Parse all kinds of bodies app$use("json body parser" = mw_json()) app$use("text body parser" = mw_text(type = c("text/plain", "application/json"))) app$use("multipart body parser" = mw_multipart()) app$use("URL encoded body parser" = mw_urlencoded()) app$use("cookie parser" = mw_cookie_parser()) # Add etags by default app$use("add etag" = mw_etag()) # Add date by default app$use("add date" = function(req, res) { res$set_header("Date", http_time_stamp()) "next" }) make_common_response <- function(req, res) { ret <- list( args = as.list(req$query), data = req$text, files = encode_files(req$files), form = req$form, headers = req$headers, json = req$json, method = req$method, path = req$path, origin = req$remote_addr, url = req$url ) } common_response <- function(req, res) { ret <- make_common_response(req, res) res$send_json(object = ret, auto_unbox = TRUE, pretty = TRUE) } # Main page app$get("/", function(req, res) { res$send_file( root = system.file(package = "webfakes", "examples", "httpbin", "assets"), "httpbin.html" ) }) # HTTP methods ========================================================= common_get <- function(req, res) { ret <- list( args = as.list(req$query), headers = req$headers, origin = req$remote_addr, path = req$path, url = req$url ) res$send_json(object = ret, auto_unbox = TRUE, pretty = TRUE) } app$get("/get", common_get) app$delete("/delete", common_response) app$patch("/patch", common_response) app$post("/post", common_response) app$put("/put", common_response) app$get("/forms/post", function(req, res) { res$send_file( root = system.file(package = "webfakes", "examples", "httpbin", "assets"), "forms-post.html" ) }) # Auth ================================================================= basic_auth <- function(req, res, error_status = 401L) { exp <- paste( "Basic", base64_encode(paste0(req$params$user, ":", req$params$passwd)) ) hdr <- req$get_header("Authorization") %||% "" if (exp == hdr) { res$send_json(list( authenticated = jsonlite::unbox(TRUE), user = jsonlite::unbox(req$params$user) )) } else { if (error_status == 401L) { res$ set_header("WWW-Authenticate", "Basic realm=\"Fake Realm\"")$ send_status(error_status) } else { res$ send_status(error_status) } } } app$get("/basic-auth/:user/:passwd", function(req, res) { basic_auth(req, res, error_status = 401L) }) app$get("/hidden-basic-auth/:user/:passwd", function(req, res) { basic_auth(req, res, error_status = 404L) }) app$get("/bearer", function(req, res) { auth <- req$get_header("Authorization") %||% "" if (! grepl("^Bearer ", auth)) { res$ set_header("WWW-Authenticate", "bearer")$ send_status(401L) } else { token <- sub("^Bearer ", "", auth) res$ send_json( list(authenticated = TRUE, token = token), auto_unbox = TRUE, pretty = TRUE ) } }) hash <- function(str, algorithm) { algo <- tolower(algorithm %||% "md5") algo <- c("md5" = "md5", "sha-256" = "sha256", "sha-512" = "sha512")[algo] if (is.na(algo)) { stop("Unknown hash algorithm for digest auth: ", algorithm) } digest::digest(str, algo = algo, serialize = FALSE) } hash1 <- function(realm, username, password, algorithm) { realm <- realm %||% "" hash(paste(collapse = ":", c( username, realm, password )), algorithm) } hash2 <- function(credentials, req, algorithm) { qop <- credentials[["qop"]] %||% "auth" query <- if (nchar(req$query_string %||% "")) paste0("?", req$query_string) req_uri <- paste0(req$path, query) if (qop == "auth") { hash(paste(collapse = ":", c( toupper(req[["method"]]), req_uri )), algorithm) } else { hash(paste0(collapse = ":", c( toupper(req[["method"]]), req_uri, hash(req$.body %||% "", algorithm) )), algorithm) } } digest_challenge_response <- function(req, qop, algorithm, stale = FALSE) { nonce <- hash( paste0(req$remote_addr, ":", unclass(Sys.time()), ":", random_id(10)), algorithm ) opaque <- hash(random_id(10), algorithm) realm <- "webfakes.r-lib.org" qop <- qop %||% "auth,auth-int" wwwauth <- paste0( "Digest ", "realm=\"", realm, "\", ", "qop=\"", qop, "\", ", "nonce=\"", nonce, "\", ", "opaque=\"", opaque, "\", ", "algorithm=", algorithm, ", ", "stale=", stale ) wwwauth } next_stale_after_value <- function(x) { x <- suppressWarnings(as.integer(x)) if (is.na(x)) "never" else as.character(x - 1L) } check_digest_auth <- function(req, credentials, user, passwd) { if (is.null(credentials)) return(FALSE) algorithm <- credentials[["algorithm"]] HA1_value <- hash1( credentials[["realm"]], credentials[["username"]], passwd, algorithm ) HA2_value <- hash2(credentials, req, algorithm) qop <- credentials[["qop"]] %||% "compat" if (! qop %in% c("compat", "auth", "auth-int")) return(FALSE) response_hash <- if (qop == "compat") { hash(paste0(c( HA1_value, credentials[["nonce"]] %||% "", HA2_value ), collapse = ":"), algorithm) } else { if (any(! c("nonce", "nc", "cnonce", "qop") %in% names(credentials))) { return(FALSE) } hash(paste0(c( HA1_value, credentials[["nonce"]], credentials[["nc"]], credentials[["cnonce"]], credentials[["qop"]], HA2_value ), collapse = ":"), algorithm) } (credentials[["response"]] %||% "") == response_hash } digest_auth <- function(req, res, qop, user, passwd, algorithm, stale_after) { require_cookie_handling <- tolower(req$query$`require-cookie` %||% "") %in% c("1", "t", "true") if (! algorithm %in% c("MD5", "SHA-256", "SHA-512")) { algorithm <- "MD5" } if (! qop %in% c("auth", "auth-int")) { qop <- NULL } authorization <- req$get_header("Authorization") credentials <- if (!is.null(authorization)) { parse_authorization_header(authorization) } if (is.null(authorization) || is.null(credentials) || tolower(credentials$scheme) != "digest" || (require_cookie_handling && is.null(req$get_header("Cookie"))) ) { wwwauth <- digest_challenge_response(req, qop, algorithm) res$ set_status(401L)$ add_cookie("stale_after", stale_after)$ add_cookie("fake", "fake_value")$ set_header("WWW-Authenticate", wwwauth)$ send("") return() } if (require_cookie_handling && (req$cookies[["fake"]] %||% "") != "fake_value") { res$ add_cookie("fake", "fake_value")$ add_status(403L)$ send_json( list(errors = "missing cookie set on challenge"), pretty = TRUE ) return() } current_nonce <- credentials$nonce stale_after_value <- req$cookies$stale_after %||% "" if (identical(current_nonce, req$cookies[["last_nonce"]] %||% "") || stale_after_value == "0") { wwwauth <- digest_challenge_response(req, qop, algorithm, TRUE) res$ set_status(401L)$ add_cookie("stale_after", stale_after)$ add_cookie("last_nonce", current_nonce)$ add_cookie("fake", "fake_value")$ set_header("WWW-Authenticate", wwwauth)$ send("") return() } if (!check_digest_auth(req, credentials, user, passwd)) { wwwauth <- digest_challenge_response(req, qop, algorithm, FALSE) res$ set_status(401L)$ add_cookie("stale_after", stale_after)$ add_cookie("last_nonce", current_nonce)$ add_cookie("fake", "fake_value")$ set_header("WWW-Authenticate", wwwauth)$ send("") return() } res$add_cookie("fake", "fake_value") if (!is.null(stale_after_value)) { res$add_cookie( "stale_after", next_stale_after_value(stale_after_value) ) } res$ send_json( list(authentication = TRUE, user = user), pretty = TRUE, auto_unbox = TRUE ) } app$get("/digest-auth/:qop/:user/:passwd", function(req, res) { qop <- req$params$qop user <- req$params$user passwd <- req$params$passwd digest_auth(req, res, qop, user, passwd, "MD5", "never") }) app$get("/digest-auth/:qop/:user/:passwd/:algorithm", function(req, res) { qop <- req$params$qop user <- req$params$user passwd <- req$params$passwd algorithm <- req$params$algorithm digest_auth(req, res, qop, user, passwd, algorithm, "never") }) app$get( "/digest-auth/:qop/:user/:passwd/:algorithm/:stale_after", function(req, res) { qop <- req$params$qop user <- req$params$user passwd <- req$params$passwd algorithm <- req$params$algorithm stale_after <- req$params$stale_after digest_auth(req, res, qop, user, passwd, algorithm, stale_after) } ) # Status codes ========================================================= app$all( new_regexp("^/status/(?[0-9][0-9][0-9])$"), function(req, res) { status <- req$params$status res$set_status(status) if (status == "418") { res$send(paste( sep = "\n", "", " -=[ teapot ]=-", "", " _...._", " .' _ _ `.", " | .\"` ^ `\". _,", " \\_;`\"---\"`|//", " | ;/", " \\_ _/", " `\"\"\"`", "" )) } else { res$send("") } } ) # Request inspection =================================================== app$get("/headers", function(req, res) { ret <- list(headers = req$headers) res$send_json(ret, auto_unbox = TRUE, pretty = TRUE) }) app$get("/ip", function(req, res) { ret <- list(origin = req$remote_addr) res$send_json(ret, auto_unbox = TRUE, pretty = TRUE) }) app$get("/user-agent", function(req, res) { ret <- list("user-agent" = req$get_header("User-Agent")) res$send_json(ret, auto_unbox = TRUE, pretty = TRUE) }) # Response inspection ================================================== app$get("/etag/:etag", function(req, res) { etag <- req$params$etag # The mw_etag() middleware is active, so we need to do this after that res_etag <- NULL res$on_response(function(req, res) { if (!is.null(res_etag)) res$set_header("ETag", res_etag) }) parse <- function(x) { x <- strsplit(x, ",", fixed = TRUE)[[1]] re_match(x, '\\s*(W/)?"?([^"]*)"?\\s*')$groups[,2] } if_none_match <- parse(req$get_header("If-None-Match") %||% "") if_match <- parse(req$get_header("If-Match") %||% "") if (length(if_none_match) > 0) { if (etag %in% if_none_match || "*" %in% if_none_match) { res$send_status(304) res_etag <- "etag" return() } } else if (length(if_match) > 0) { if ((! etag %in% if_match) && (!"*" %in% if_match)) { res$send_status(412) return() } } res_etag <- etag common_get(req, res) }) rsp_hdrs <- function(req, res) { obj <- structure(list(), names = character()) for (i in seq_along(req$query)) { key <- names(req$query)[i] res$add_header(key, req$query[[i]]) obj[[key]] <- c(obj[[key]], req$query[[i]]) } res$send_json(object = obj, auto_unbox = TRUE) } app$get("/response-headers", rsp_hdrs) app$post("/response-headers", rsp_hdrs) app$get("/cache", function(req, res) { if (is.null(req$get_header("If-Modified-Since")) && is.null(req$get_header("If-None-Match"))) { res$set_header("Last-Modified", http_time_stamp()) # etag is added by default common_response(req, res) } else { res$send_status(304) } }) app$get("/cache/:value", function(req, res) { value <- suppressWarnings(as.integer(req$params$value)) if (is.na(value)) { "next" } else { res$set_header( "Cache-Control", sprintf("public, max-age=%d", value) ) common_response(req, res) } }) # Response formats ===================================================== app$get("/deny", function(req, res) { res$ set_type("text/plain")$ send_file( root = system.file(package = "webfakes"), file.path("examples", "httpbin", "data", "deny.txt") ) }) app$get("/brotli", function(req, res) { ret <- make_common_response(req, res) ret$brotli <- TRUE json <- jsonlite::toJSON(ret, auto_unbox = TRUE, pretty = TRUE) data <- charToRaw(json) datax <- brotli::brotli_compress(data) res$ set_type("application/json")$ set_header("Content-Encoding", "brotli")$ send(datax) }) app$get("/gzip", function(req, res) { ret <- make_common_response(req, res) ret$gzipped <- TRUE json <- jsonlite::toJSON(ret, auto_unbox = TRUE, pretty = TRUE) tmp <- tempfile() on.exit(unlink(tmp), add = TRUE) con <- file(tmp, open = "wb") con2 <- gzcon(con) writeBin(charToRaw(json), con2) flush(con2) close(con2) gzipped <- readBin(tmp, "raw", file.info(tmp)$size) res$ set_type("application/json")$ set_header("Content-Encoding", "gzip")$ send(gzipped) }) app$get("/deflate", function(req, res) { ret <- make_common_response(req, res) ret$deflated <- TRUE json <- jsonlite::toJSON(ret, auto_unbox = TRUE, pretty = TRUE) data <- charToRaw(json) datax <- zip::deflate(data) res$ set_type("application/json")$ set_header("Content-Encoding", "deflate")$ send(datax$output) }) app$get("/encoding/utf8", function(req, res) { res$ set_type("text/html; charset=utf-8")$ send_file( root = system.file(package = "webfakes"), file.path("examples", "httpbin", "data", "utf8.html") ) }) app$get("/html", function(req, res) { res$send_file( root = system.file(package = "webfakes"), file.path("examples", "httpbin", "data", "example.html") ) }) app$get("/json", function(req, res) { res$send_file( root = system.file(package = "webfakes"), file.path("examples", "httpbin", "data", "example.json") ) }) app$get("/robots.txt", function(req, res) { res$send_file( root = system.file(package = "webfakes"), file.path("examples", "httpbin", "data", "robots.txt") ) }) app$get("/xml", function(req, res) { res$send_file( root = system.file(package = "webfakes"), file.path("examples", "httpbin", "data", "example.xml") ) }) # Dynamic data ========================================================= app$get(list("/base64", new_regexp("/base64/(?[\\+/=a-zA-Z0-9]*)")), function(req, res) { value <- req$params$value %||% "" if (value == "") value <- "RXZlcnl0aGluZyBpcyBSc29tZQ==" plain <- charToRaw(base64_decode(value)) res$ set_type("application/octet-stream")$ send(plain) }) app$get("/bytes/:n", function(req, res) { n <- suppressWarnings(as.integer(req$params$n)) if (is.na(n)) { return("next") } else { n <- min(n, 10000) bytes <- as.raw(as.integer(floor(stats::runif(n, min=0, max=256)))) res$ set_type("application/octet-stream")$ send(bytes) } }) app$all(new_regexp("/delay/(?[0-9\\.]+)$"), function(req, res) { delay <- suppressWarnings(as.numeric(req$params$delay)) if (is.na(delay)) { return("next") } else if (is.null(res$locals$seen)) { res$locals$seen <- TRUE delay <- min(delay, 10) res$delay(delay) } else if (req$method == "head") { res$send_status(200L) } else { common_response(req, res) } }) app$get("/drip", function(req, res) { # First time? if (is.null(res$locals$drip)) { duration <- as.double(req$query$duration %||% 2) numbytes <- as.integer(req$query$numbytes %||% 10) code <- as.integer(req$query$code %||% 200L) delay <- as.double(req$query$delay %||% 0) # how much to wait between messages, at least 10ms pause <- max(duration / numbytes, 0.01) # how many messages nummsg <- duration / pause + 1 # how big is a message, at least a byte msgsize <- max(floor(numbytes / nummsg), 1) res$locals$drip <- list( tosend = numbytes, msgsize = msgsize, pause = pause ) res$ set_header("Content-Length", numbytes)$ set_header("Content-Type", "application/octet-stream")$ set_status(code) if (delay > 0) return(res$delay(delay)) } len <- min(res$locals$drip$tosend, res$locals$drip$msgsize) res$write(strrep("*", len)) res$locals$drip$tosend <- res$locals$drip$tosend - len if (res$locals$drip$tosend == 0) { res$send("") } else { res$delay(res$locals$drip$pause) } }) app$get(new_regexp("^/stream/(?[0-9]+)$"), function(req, res) { n <- suppressWarnings(as.integer(req$params$n)) n <- min(n, 100) if (length(n) == 0 || is.na(n)) return("next") msg <- make_common_response(req, res)[c("url", "args", "headers", "origin")] res$set_type("application/json") for (i in seq_len(n)) { msg$id <- i - 1L txt <- paste0(jsonlite::toJSON(msg, auto_unbox = TRUE), "\n") res$send_chunk(charToRaw(txt)) } }) app$get(new_regexp("^/stream-bytes/(?[0-9]+)$"), function(req, res) { n <- suppressWarnings(as.integer(req$params$n)) n <- min(n, 100 * 1024) seed <- suppressWarnings(as.integer(req$query$seed %||% 42)) chunk_size <- suppressWarnings(as.integer(req$query$chunk_size %||% 10240)) if (length(n) == 0 || is.na(n) || length(seed) == 0 || is.na(seed) || length(chunk_size) == 0 || is.na(chunk_size)) return("next") oldseed <- .GlobalEnv$.Random.seed on.exit(set.seed(oldseed)) set.seed(seed) bytes <- as.raw(as.integer(floor(stats::runif(n, min=0, max=256)))) nc <- ceiling(n / chunk_size) for (i in seq_len(nc)) { from <- (i-1)*chunk_size + 1 to <- min(length(bytes), i * chunk_size) res$send_chunk(bytes[from:to]) } }) re_range <- new_regexp("^/range/(?[0-9]+)$") # This is not in httpbin, but it is handy to get the size of the # response, and to see whether the server supports ranges app$head(re_range, function(req, res) { numbytes <- suppressWarnings(as.integer(req$params$n)) if (length(numbytes) == 0 || is.na(numbytes)) { return("next") } res$ set_header("ETag", paste0("range", numbytes))$ set_header("Accept-Ranges", "bytes")$ set_header("Content-Length", numbytes)$ send_status(200L) }) app$get(re_range, function(req, res) { if (is.null(res$locals$range)) { numbytes <- suppressWarnings(as.integer(req$params$n)) if (length(numbytes) == 0 || is.na(numbytes)) { return("next") } if (numbytes < 0 || numbytes > 100 * 1024) { res$ set_header("ETag", paste0("range", numbytes))$ set_header("Accept-Ranges", "bytes")$ set_status(404L)$ send("number of bytes must be in the range (0, 102400].") return() } chunk_size <- max(1, as.integer(req$query$chunk_size %||% (10 * 1024))) duration <- as.integer(req$query$duration %||% 0) pause_per_byte <- duration / numbytes if (duration == 0) { chunk_size <- numbytes } ranges <- parse_range(req$get_header("Range")) # just like httpbin, we do not support multiple ranges if (NROW(ranges) != 1) { ranges <- NULL } # This is not exactly the same as httpbin, but rather follows # https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests # and also how web servers seem to behave. # # In particular, in these cases we return the full response: # - no Range header, # - invalid Range header syntax, # - overlapping Range header ranges # # Otherwise, if a range is outside of the size of the response, we # return a 416 response. if (!is.null(ranges)) { ranges[ranges == Inf] <- numbytes if (any(ranges[,2] >= numbytes)) { res$ set_header("ETag", paste0("range", numbytes))$ set_header("Accept-Ranges", "bytes")$ set_header("Content-Range", paste0("bytes */", numbytes))$ set_header("Content-Length", 0L)$ send_status(416L) return() } } # we need the response for sure abc <- paste(letters, collapse = "") bytes <- substr(strrep(abc, numbytes / nchar(abc) + 1), 1, numbytes) res$locals$range <- list( bytes = bytes, chunk_size = chunk_size, pause_per_byte = pause_per_byte ) # First part, so send status and headers if (is.null(ranges)) { res$ set_header("ETag", paste0("range", numbytes))$ set_header("Accept-Ranges", "bytes")$ set_header("Content-Length", numbytes)$ set_status(200L) } else if (nrow(ranges) == 1) { # A single range res$ set_header("ETag", paste0("range", numbytes))$ set_header("Accept-Ranges", "bytes")$ set_header( "Content-Range", sprintf("bytes=%d-%d/%d", ranges[1, 1], ranges[1, 2], numbytes) )$ set_header("Content-Length", ranges[1, 2] - ranges[1, 1] + 1L)$ set_status(206L) # This is all we need to send res$locals$range$bytes <- substr( bytes, ranges[1, 1] + 1, ranges[1, 2] + 1L ) } else { # This cannot happen now, we do not support multiple ranges # Maybe later } } # send a part chunk_size <- res$locals$range$chunk_size pause <- res$locals$range$pause_per_byte tosend <- substr(res$locals$range$bytes, 1, chunk_size) res$locals$range$bytes <- substr( res$locals$range$bytes, chunk_size + 1L, nchar(res$locals$range$bytes) ) res$write(tosend) if (pause * nchar(tosend) > 0) { res$delay(pause * nchar(tosend)) } }) app$get("/uuid", function(req, res) { ret <- list(uuid = uuid_random()) res$send_json(ret, auto_unbox = TRUE, pretty = TRUE) }) app$get(new_regexp("^/links/(?[0-9]+)(/(?[0-9]+))?$"), function(req, res) { n <- suppressWarnings(as.integer(req$params$n)) o <- suppressWarnings(as.integer(req$params$offset)) if (length(o) == 0 || is.na(o)) o <- 1 if (length(n) == 0 || is.na(n)) return("next") n <- min(max(1, n), 200) o <- min(max(1, o), n) links <- sprintf("%d", n, 1:n, 1:n) links[o] <- o html <- paste0( "Links", paste(links, collapse = " "), "" ) res$ set_type("html")$ send(html) }) # Cookies ============================================================== app$get("/cookies", function(req, res) { cks <- req$cookies res$send_json( object = list(cookies = cks), auto_unbox = TRUE, pretty = TRUE ) }) app$get("/cookies/set/:name/:value", function(req, res) { res$add_cookie(req$params$name, req$params$value) res$redirect("/cookies", 302L) }) app$get("/cookies/set", function(req, res) { for (n in names(req$query)) { res$add_cookie(n, req$query[[n]]) } res$redirect("/cookies", 302L) }) app$get("/cookies/delete", function(req, res) { for (n in names(req$query)) { res$clear_cookie(n) } res$redirect("/cookies", 302L) }) # Images =============================================================== app$get("/image", function(req, res) { act <- req$get_header("Accept") ok <- c( "image/webp", "image/svg+xml", "image/jpeg", "image/png", "image/*" ) msg <- list( message = "Client did not request a supported media type.", accept = ok ) if (is.null(act) || ! act %in% ok) { res$ set_status(406)$ set_type("application/json")$ send_json(msg) } else { fls <- c( "image/webp" = "Rlogo.webp", "image/svg+xml" = "Rlogo.svg", "image/jpeg" = "Rlogo.jpeg", "image/png" = "Rlogo.png", "image/*" = "Rlogo.png" ) res$send_file( root = system.file(package = "webfakes"), file.path("examples", "httpbin", "images", fls[act]) ) } }) app$get(new_regexp("/image/(?jpeg|png|svg|webp)"), function(req, res) { filename <- paste0("Rlogo.", req$params$format) res$send_file( root = system.file(package = "webfakes"), file.path("examples", "httpbin", "images", filename) ) }) # Redirects ============================================================ app$get("/absolute-redirect/:n", function(req, res) { n <- suppressWarnings(as.integer(req$params$n)) if (is.na(n)) { return("next") } else { if (n == 1) { url <- sub("/absolute-redirect/[0-9]+$", "/get", req$url) } else { n <- min(n, 5) url <- paste0(sub("/[0-9]+$", "/", req$url), n - 1) } res$redirect(url, 302L) } }) app$get(c("/redirect/:n", "/relative-redirect/:n"), function(req, res) { n <- suppressWarnings(as.integer(req$params$n)) if (is.na(n)) { return("next") } else { if (n == 1) { url <- sub("/redirect/[0-9]+$", "/get", req$path) url <- sub("/relative-redirect/[0-9]+$", "/get", url) } else { n <- min(n, 5) url <- paste0(sub("/[0-9]+$", "/", req$path), n - 1) } res$redirect(url, 302L) } }) app$all("/redirect-to", function(req, res) { res$redirect(req$query$url, req$query$status_code %||% 302) }) # Anything ============================================================= app$all(new_regexp("^/anything"), common_response) app } webfakes/R/print.R0000644000176200001440000001211114172041777013527 0ustar liggesusers format_named_list <- function(name, data) { c(paste0(name, ":"), if (length(data)) paste0(" ", names(data), ": ", data) ) } format_path <- function(patterns) { # Make sure patterns is a list if (inherits(patterns, "webfakes_regexp")) { patterns <- list(patterns) } else if (is.character(patterns)) { patterns <- as.list(patterns) } paste(collapse = ", ", vapply(patterns, format, character(1))) } #' @export format.webfakes_app <- function(x, ...) { header <- "" data <- vapply(x$.stack, FUN.VALUE = character(1), function(x) { name <- if (nchar(x$name %||% "")) paste0(" # ", x$name) paste0(" ", x$method, " ", format_path(x$path), name) }) methods <- c( " all(path, ...) # add route for *all* HTTP methods", " delete(path, ...) # add route for DELETE", " engine(ext, engine) # add template engine for file extension", " head(path, ...) # add route for HEAD", " listen(port) # start web app on port", " patch(path, ...) # add route for PATCH", " post(path, ...) # add route for POST", " put(path, ...) # add route for PUT", " use(...) # add middleware", " locals # app-wide shared data" ) help <- "# see ?webfakes_app for all methods" c(header, "routes:", data, "fields and methods:", methods, help) } #' @export print.webfakes_app <- function(x, ...) { cat(format(x, ...), sep = "\n") invisible(x) } #' @export format.webfakes_request <- function(x, ...) { header <- "" data <- c( "method:", paste0(" ", x$method), "url:", paste0(" ", x$url), "client:", paste0(" ", x$remote_addr), format_named_list("query", x$query), format_named_list("headers", x$headers) ) methods <- c( " app # the webfakes_app the request belongs to", " headers # HTTP request headers", " hostname # server hostname, the Host header", " method # HTTP method of request (lowercase)", " path # server path", " protocol # http or https", " query_string # raw query string without '?'", " query # named list of query parameters", " remote_addr # IP address of the client", " url # full URL of the request", " get_header(field) # get a request header" ) help <- " # see ?webfakes_request for details" c(header, data, "fields and methods:", methods, help) } #' @export print.webfakes_request <- function(x, ...) { cat(format(x, ...), sep = "\n") invisible(x) } #' @export format.webfakes_response <- function(x, ...) { header <- "" methods <- c( " app # the webfakes_app the response belongs to", " locals # response-wide shared data", " get_header(field) # query response header", " on_response(fun) # call handler function for complete response", " redirect(path, status) # send redirect response", " render(view, locals) # render template", " send(body) # send text or raw data", " send_file(path, root) # send a file (automatic Content-Type)", " send_json(object, text, ...)", " # send JSON data", " send_status(status) # send HTTP status and empty body", " set_header(field, value) # set a response header", " set_status(status) # set response status code", " set_type(type) # set Content-Type" ) help <- " # see ?webfakes_response for details" c(header, "fields and methods:", methods, help) } #' @export print.webfakes_response <- function(x, ...) { cat(format(x, ...), sep = "\n") invisible(x) } #' @export format.webfakes_regexp <- function(x, ...) { paste0(" ", encodeString(x, quote = "\"")) } #' @export print.webfakes_regexp <- function(x, ...) { cat(format(x, ...), sep = "\n") invisible(x) } #' @export format.webfakes_app_process <- function(x, ...) { header <- "" state <- x$get_state() data <- c( "state:", paste0(" ", state), "auto_start:", paste0(" ", x$.auto_start), "process id:", paste0(" ", if (state == "not running") "none" else x$.process$get_pid()), "http url:", paste0(" ", if (state == "live") x$url() else "NA") ) methods <- c( " get_app() # get the app object", " get_port() # query port of the app", " get_state() # query web server process state", " local_env(envvars) # set temporary environment variables", " start() # start the app", " url(path, query) # query url for an api path", " stop() # stop web server process" ) help <- "# see ?webfakes_app_process for details" c(header, data, "fields and methods:", methods, help) } #' @export print.webfakes_app_process <- function(x, ...) { cat(format(x, ...), sep = "\n") invisible(x) } webfakes/R/local-app-process.R0000644000176200001440000000121714172041777015724 0ustar liggesusers #' App process that is cleaned up automatically #' #' You can start the process with an explicit `$start()` call. #' Alternatively it starts up at the first `$url()` or `$get_port()` #' call. #' #' @inheritParams new_app_process #' @param ... Passed to [new_app_process()]. #' @param .local_envir The environment to attach the process cleanup to. #' Typically a frame. When this frame finishes, the process is stopped. #' #' @export #' @seealso [new_app_process()] for more details. local_app_process <- function(app, ..., .local_envir = parent.frame()) { proc <- new_app_process(app, ...) withr::defer(proc$stop(), envir = .local_envir) proc } webfakes/R/rematch2.R0000644000176200001440000000172014172041777014104 0ustar liggesusers re_match <- function(text, pattern, perl = TRUE, ...) { stopifnot(is.character(pattern), length(pattern) == 1, !is.na(pattern)) text <- as.character(text) match <- regexpr(pattern, text, perl = perl, ...) start <- as.vector(match) length <- attr(match, "match.length") end <- start + length - 1L matchstr <- substring(text, start, end) matchstr[ start == -1 ] <- NA_character_ empty <- data.frame(stringsAsFactors = FALSE, .text = text)[, numeric()] res <- list(match = !is.na(matchstr), groups = empty) if (!is.null(attr(match, "capture.start"))) { gstart <- attr(match, "capture.start") glength <- attr(match, "capture.length") gend <- gstart + glength - 1L groupstr <- substring(text, gstart, gend) groupstr[ gstart == -1 ] <- NA_character_ dim(groupstr) <- dim(gstart) res$groups <- cbind(groupstr, res$groups, stringsAsFactors = FALSE) names(res$groups) <- attr(match, "capture.names") } res } webfakes/R/cleancall.R0000644000176200001440000000015314172041777014314 0ustar liggesusers call_with_cleanup <- function(ptr, ...) { .Call(c_cleancall_call, pairlist(ptr, ...), parent.frame()) } webfakes/R/server.R0000644000176200001440000001423414740243712013703 0ustar liggesusers server_start <- function(opts = server_opts()) { ports <- paste0(opts$interfaces, ":", opts$port %||% "0", collapse = ",") if (!is.na(opts$access_log_file)) { mkdirp(dirname(opts$access_log_file)) file.create(opts$access_log_file) } if (!is.na(opts$error_log_file)) { mkdirp(dirname(opts$error_log_file)) file.create(opts$error_log_file) } throttle <- paste0( "*=", if (opts$throttle == Inf) "0" else opts$throttle ) options <- c( "listening_ports" = ports, "num_threads" = opts$num_threads, "enable_keep_alive" = c("no", "yes")[[opts$enable_keep_alive + 1]], "access_log_file" = opts$access_log_file %|NA|% "", "error_log_file" = opts$error_log_file %|NA|% "", "tcp_nodelay" = c("0", "1")[[opts$tcp_nodelay + 1]], "throttle" = throttle, # These are not configurable currently "request_timeout_ms" = "100000", "enable_auth_domain_check" = "no", "decode_url" = if (opts$decode_url) "yes" else "no" ) srv <- call_with_cleanup(c_server_start, options) attr(srv, "options") <- opts srv } #' Webfakes web server options #' #' @param remote Meta-option. If set to `TRUE`, webfakes uses slightly #' different defaults, that are more appropriate for a background #' server process. #' @param port Port to start the web server on. Defaults to a randomly #' chosen port. #' @param num_threads Number of request handler threads to use. Typically #' you don't need more than one thread, unless you run test cases in #' parallel or you make concurrent HTTP requests. #' @param interfaces The network interfaces to listen on. Being a test #' web server, it defaults to the localhost. Only bind to a public #' interface if you know what you are doing. webfakes was not designed #' to serve public web pages. #' @param enable_keep_alive Whether the server keeps connections alive. #' @param access_log_file `TRUE`, `FALSE`, or a path. See 'Logging' #' below. #' @param error_log_file `TRUE`, `FALSE`, or a path. See 'Logging' #' below. #' @param tcp_nodelay if `TRUE` then packages will be sent as soon as #' possible, instead of waiting for a full buffer or timeout to occur. #' @param throttle Limit download speed for clients. If not `Inf`, #' then it is the maximum number of bytes per second, that is sent to #' as connection. #' @param decode_url Whether the server should automatically decode #' URL-encodded URLs. If `TRUE` (the default), `/foo%2fbar` will be #' converted to `/foo/bar` automatically. If `FALSE`, URLs as not #' URL-decoded. #' @return List of options that can be passed to `webfakes_app$listen()` #' (see [new_app()]), and [new_app_process()]. #' #' @section Logging: #' #' * For `access_log_file`, `TRUE` means `/access.log`. #' * For `error_log_file`, `TRUE` means `/error.log`. #' #' `` is set to the contents of the `WEBFAKES_LOG_DIR` #' environment variable, if it is set. Otherwise it is set to #' `/webfakes` for local apps and `//webfakes` for #' remote apps (started with `new_app_procss()`). #' #' `` is the session temporary directory of the _main process_. #' #' `` is the process id of the subprocess. #' #' @export #' @examples #' # See the defaults #' server_opts() server_opts <- function(remote = FALSE, port = NULL, num_threads = 1, interfaces = "127.0.0.1", enable_keep_alive = FALSE, access_log_file = remote, error_log_file = TRUE, tcp_nodelay = FALSE, throttle = Inf, decode_url = TRUE) { log_dir <- Sys.getenv("WEBFAKES_LOG_DIR", file.path(tempdir(), "webfakes")) if (isTRUE(access_log_file)) { if (remote) { access_log_file <- file.path(log_dir, "%p", "access.log") } else { access_log_file <- file.path(log_dir, "access.log") } } else if (isFALSE(access_log_file)) { access_log_file <- NA_character_ } if (isTRUE(error_log_file)) { if (remote) { error_log_file <- file.path(log_dir, "%p", "error.log") } else { error_log_file <- file.path(log_dir, "error.log") } } else if (isFALSE(error_log_file)) { error_log_file <- NA_character_ } rm(log_dir) as.list(environment()) } server_get_ports <- function(srv) { as.data.frame(call_with_cleanup(c_server_get_ports, srv)) } server_stop <- function(srv) { invisible(call_with_cleanup(c_server_stop, srv)) } server_poll <- function(srv, cleanup = TRUE) { while (TRUE) { tryCatch( return(call_with_cleanup(c_server_poll, srv, cleanup)), error = function(err) { cat(as.character(err), file = stderr()) stop(new_webfakes_error()) } ) } } response_send <- function(req) { tryCatch( call_with_cleanup(c_response_send, req), error = function(err) { cat(as.character(err), file = stderr()) stop(new_webfakes_error()) } ) invisible(NULL) } response_send_chunk <- function(req, data) { tryCatch( call_with_cleanup(c_response_send_chunk, req, data), error = function(err) { cat(as.character(err), file = stderr()) stop(new_webfakes_error()) } ) invisible(NULL) } response_send_error <- function(req, msg, status) { tryCatch( call_with_cleanup(c_response_send_error, req, msg, status), error = function(err) { cat(as.character(err), file = stderr()) stop(new_webfakes_error()) } ) invisible(NULL) } response_delay <- function(req, secs) { tryCatch( call_with_cleanup(c_response_delay, req, secs), error = function(err) { cat(as.character(err), file = stderr()) stop(new_webfakes_error()) } ) invisible(NULL) } response_write <- function(req, data) { tryCatch( call_with_cleanup(c_response_write, req, data), error = function(err) { cat(as.character(err), file = stderr()) stop(new_webfakes_error()) } ) invisible(NULL) } new_webfakes_error <- function(...) { structure( list(message = "error in webfakes connection", ...), class = c("webfakes_error", "error", "condition") ) } webfakes/R/app-process.R0000644000176200001440000001730614740243712014634 0ustar liggesusers #' Run a webfakes app in another process #' #' Runs an app in a subprocess, using [callr::r_session]. #' #' @param app `webfakes_app` object, the web app to run. #' @param port Port to use. By default the OS assigns a port. #' @param opts Server options. See [server_opts()] for the defaults. #' @param start Whether to start the web server immediately. If this is #' `FALSE`, and `auto_start` is `TRUE`, then it is started as neeed. #' @param auto_start Whether to start the web server process automatically. #' If `TRUE` and the process is not running, then `$start()`, #' `$get_port()` and `$url()` start the process. #' @param process_timeout How long to wait for the subprocess to start, in #' milliseconds. #' @param callr_opts Options to pass to [callr::r_session_options()] #' when setting up the subprocess. #' @return A `webfakes_app_process` object. #' #' ## Methods #' #' The `webfakes_app_process` class has the following methods: #' #' ```r #' get_app() #' get_port() #' stop() #' get_state() #' local_env(envvars) #' url(path = "/", query = NULL) #' ``` #' #' * `envvars`: Named list of environment variables. The `{url}` substring #' is replaced by the URL of the app. #' * `path`: Path to return the URL for. #' * `query`: Additional query parameters, a named list, to add to the URL. #' #' `get_app()` returns the app object. #' #' `get_port()` returns the port the web server is running on. #' #' `stop()` stops the web server, and also the subprocess. If the error #' log file is not empty, then it dumps its contents to the screen. #' #' `get_state()` returns a string, the state of the web server: #' * `"not running"` the server is not running (because it was stopped #' already). #' * `"live"` means that the server is running. #' * `"dead"` means that the subprocess has quit or crashed. #' #' `local_env()` sets the given environment variables for the duration of #' the app process. It resets them in `$stop()`. Webfakes replaces `{url}` #' in the value of the environment variables with the app URL, so you can #' set environment variables that point to the app. #' #' `url()` returns the URL of the web app. You can use the `path` #' parameter to return a specific path. #' #' @aliases webfakes_app_process #' @seealso [local_app_process()] for automatically cleaning up the #' subprocess. #' @export #' @examples #' app <- new_app() #' app$get("/foo", function(req, res) { #' res$send("Hello world!") #' }) #' #' proc <- new_app_process(app) #' url <- proc$url("/foo") #' resp <- curl::curl_fetch_memory(url) #' cat(rawToChar(resp$content)) #' #' proc$stop() new_app_process <- function(app, port = NULL, opts = server_opts(remote = TRUE), start = FALSE, auto_start = TRUE, process_timeout = NULL, callr_opts = NULL) { app; port; opts; start; auto_start; process_timeout; callr_opts # WTF tmp <- tempfile() sink(tmp); print(app$.stack); sink(NULL) unlink(tmp) process_timeout <- process_timeout %||% as.integer(Sys.getenv( "R_WEBFAKES_PROCESS_TIMEOUT", if (Sys.getenv("R_COVR") == "true") 600000 else 5000 )) self <- new_object( "webfakes_app_process", start = function() { self$.app <- app callr_opts <- do.call(callr::r_session_options, as.list(callr_opts)) self$.process <- callr::r_session$new(callr_opts, wait = TRUE) self$.process$call( args = list(app, port, opts), function(app, port, opts) { library(webfakes) .GlobalEnv$app <- app app$listen(port = port, opts = opts) } ) if (self$.process$poll_process(process_timeout) != "ready") { self$.process$kill() stop("webfakes app subprocess did not start :(") } msg <- self$.process$read() if (msg$code == 200 && !is.null(msg$error)) { msg$error$message <- paste0( "failed to start webfakes app process: ", msg$error$message ) stop(msg$error) } if (msg$code != 301) { stop("Unexpected message from webfakes app subprocess. ", "Report a bug please.") } self$.port <- msg$message$port self$.access_log <- msg$message$access_log self$.error_log <- msg$message$error_log self$.set_env() invisible(self) }, get_app = function() self$.app, get_port = function() { if (self$get_state() == "not running" && auto_start) self$start() self$.port }, stop = function() { self$.reset_env() if (is.null(self$.process)) return(invisible(self)) if (!self$.process$is_alive()) { status <- self$.process$get_exit_status() out <- err <- NULL try_silently(out <- self$.process$read_output()) try_silently(err <- self$.process$read_error()) cat0("webfakes process dead, exit code: ", status, "\n") if (!is.null(out)) cat0("stdout:", out, "\n") if (!is.null(err)) cat0("stderr:", err, "\n") } # The details are important here, for the sake of covr, # so that we can test the webfakes package itself. # 1. The subprocess serving the app is in Sys.sleep(), which we # need to interrupt first. # 2. Then we need to read out the result of that $call() # (i.e. the interruption), because otherwise the subprocess is # stuck at a blocking write() system call, and cannot be # interrupted in the $close() call, and will be killed, and # then it cannot write out the coverage results. # 3. Once we $read(), we can call $close() because that will # close the standard input of the subprocess, which is reading # the standard input, so it will quit. self$.process$interrupt() self$.process$poll_process(1000) try_silently(self$.process$read()) try_silently(self$.process$close()) self$.print_errors() self$.process <- NULL invisible(self) }, get_state = function() { if (is.null(self$.process)) { "not running" } else if (self$.process$is_alive()) { "live" } else { "dead" } }, local_env = function(envvars) { if (!is.null(self$.process)) { local <- unlist(envvars) local[] <- gsub("{url}", self$url(), local, fixed = TRUE) self$.old_env <- c(self$.old_env, set_envvar(local)) } else { self$.local_env <- utils::modifyList( as.list(self$.local_env), as.list(envvars) ) } invisible(self) }, .set_env = function() { local <- unlist(self$.local_env) local[] <- gsub("{url}", self$url(), local, fixed = TRUE) self$.old_env <- set_envvar(local) invisible(self) }, .reset_env = function() { if (!is.null(self$.old_env)) { set_envvar(self$.old_env) self$.old_env <- NULL } }, url = function(path = "/", query = NULL) { if (self$get_state() == "not running" && auto_start) self$start() if (!is.null(query)) { query <- paste0("?", paste0(names(query), "=", query, collapse = "&")) } paste0("http://127.0.0.1:", self$.port, path, query) }, .process = NULL, .app = NULL, .port = NULL, .old_env = NULL, .access_log = NA_character_, .error_log = NA_character_, .auto_start = auto_start, .print_errors = function() { if (!is.na(self$.error_log) && file.exists(self$.error_log) && file.info(self$.error_log)$size > 0) { err <- readLines(self$.error_log, warn = FALSE) cat("webfakes web server errors:\n") cat(err, sep = "\n") } } ) reg.finalizer(self, function(x) x$.reset_env()) if (start) self$start() self } webfakes/R/mw-cookie-parser.R0000644000176200001440000000150614740243712015557 0ustar liggesusers #' Middleware to parse Cookies #' #' Adds the cookies as the `cookies` element of the request object. #' #' It ignores cookies in an invalid format. It ignores duplicate cookies: #' if two cookies have the same name, only the first one is included. #' #' @return Handler function. #' #' @family middleware #' @export mw_cookie_parser <- function() { function(req, res) { ch <- req$get_header("Cookie") %||% "" req$cookies <- parse_cookies(ch) "next" } } parse_cookies <- function(x) { parts <- strsplit(x, ";", fixed = TRUE)[[1]] dict <- structure(list(), names = character()) lapply(parts, function(ck) { ck <- trimws(ck) key <- sub("^([^=]+)=.*$", "\\1", ck) if (key == ck) return() if (!is.null(dict[[key]])) return() value <- sub("^[^=]+=", "", ck) dict[[key]] <<- value }) dict } webfakes/R/path.R0000644000176200001440000000330214317560325013325 0ustar liggesusers #' Create a new regular expression to use in webfakes routes #' #' Note that webfakes uses PERL regular expressions. #' #' @details #' As R does not have data type or class for regular expressions, #' you can use `new_regexp()` to mark a string as a regular expression, #' when adding routes. #' #' @param x String scalar containing a regular expression. #' @return String with class `webfakes_regexp`. #' #' @aliases webfakes_regexp #' @seealso The 'Path specification' and 'Path parameters' chapters #' of the manual of [new_app()]. #' @export #' @examples #' new_regexp("^/api/match/(?.*)$") new_regexp <- function(x) structure(x, class = "webfakes_regexp") path_match <- function(method, path, handler) { if (handler$method == "use") return(TRUE) if ((! handler$method %in% c("all", method)) && !(handler$method == "get" && method == "head")) return(FALSE) pattern_match(path, handler$path) } pattern_match <- function(path, patterns) { # Make sure patterns is a list if (inherits(patterns, "webfakes_regexp")) { patterns <- list(patterns) } else if (is.character(patterns)) { patterns <- as.list(patterns) } for (p in patterns) { if (!inherits(p, "webfakes_regexp") && grepl(":", p)) { p <- path_to_regexp(p) } if (inherits(p, "webfakes_regexp")) { m <- re_match(path, p) if (m$match) return(list(params = as.list(m$groups))) } else { if (path == p) return(TRUE) } } FALSE } path_to_regexp <- function(path) { tokens <- strsplit(path, "/")[[1]] keys <- grep("^:", tokens) reg <- tokens reg[keys] <- paste0("(?<", substring(tokens[keys], 2), ">[-A-Za-z0-9_]+)") new_regexp(paste0("^", paste(reg, collapse = "/"), "$")) } webfakes/R/mw-etag.R0000644000176200001440000000215014740243712013730 0ustar liggesusers #' Middleware that add an `ETag` header to the response #' #' If the response already has an `ETag` header, then it is kept. #' #' This middleware handles the `If-None-Match` headers, and it sets the #' status code of the response to 304 if `If-None-Match` matches the #' `ETag`. It also removes the response body in this case. #' #' @param algorithm Checksum algorithm to use. Only `"crc32"` is #' implemented currently. #' #' @return Handler function. #' #' @family middleware #' @export #' @examples #' app <- new_app() #' app$use(mw_etag()) #' app mw_etag <- function(algorithm = "crc32") { if (algorithm != "crc32") { stop("Only the 'crc32' algorithm is implemented in `mw_etag()`") } function(req, res) { do <- function(req, res) { etag <- res$get_header("ETag") if (is.null(etag)) { etag <- paste0("\"", crc32(res$.body), "\"") res$set_header("ETag", etag) } req_etag <- req$get_header("If-None-Match") if (!is.null(req_etag) && req_etag == etag) { res$.body <- NULL res$set_status(304) } } res$on_response(do) "next" } } webfakes/R/base64.R0000644000176200001440000000661614172041777013474 0ustar liggesusers XX <- 255L EQ <- 254L INVALID <- XX index_64 <- as.integer(c( XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,62, XX,XX,XX,63, 52,53,54,55, 56,57,58,59, 60,61,XX,XX, XX,EQ,XX,XX, XX, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14, 15,16,17,18, 19,20,21,22, 23,24,25,XX, XX,XX,XX,XX, XX,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40, 41,42,43,44, 45,46,47,48, 49,50,51,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX, XX,XX,XX,XX )) base64_decode <- function(x) { if (is.character(x)) { x <- charToRaw(x) } len <- length(x) idx <- 1 c <- integer(4) out <- raw() while(idx <= len) { i <- 1 while(i <= 4) { uc <- index_64[[as.integer(x[[idx]]) + 1L]] idx <- idx + 1 if (uc != INVALID) { c[[i]] <- uc i <- i + 1 } if (idx > len) { if (i <= 4) { if (i <= 2) return(rawToChar(out)) if (i == 3) { c[[3]] <- EQ c[[4]] <- EQ } break } } } if (c[[1]] == EQ || c[[2]] == EQ) { break } #print(sprintf("c1=%d,c2=%d,c3=%d,c4=%d\n", c[1],c[2],c[3],c[4])) out[[length(out) + 1]] <- as.raw(bitwOr(bitwShiftL(c[[1]], 2L), bitwShiftR(bitwAnd(c[[2]], 0x30), 4L))) if (c[[3]] == EQ) { break } out[[length(out) + 1]] <- as.raw(bitwOr(bitwShiftL(bitwAnd(c[[2]], 0x0F), 4L), bitwShiftR(bitwAnd(c[[3]], 0x3C), 2L))) if (c[[4]] == EQ) { break } out[[length(out) + 1]] <- as.raw(bitwOr(bitwShiftL(bitwAnd(c[[3]], 0x03), 6L), c[[4]])) } rawToChar(out) } basis64 <- charToRaw(paste(c(LETTERS, letters, 0:9, "+", "/"), collapse = "")) base64_encode <- function(x) { if (is.character(x)) { x <- charToRaw(x) } len <- length(x) rlen <- floor((len + 2L) / 3L) * 4L out <- raw(rlen) ip <- op <- 1L c <- integer(4) while (len > 0L) { c[[1]] <- as.integer(x[[ip]]) ip <- ip + 1L if (len > 1L) { c[[2]] <- as.integer(x[ip]) ip <- ip + 1L } else { c[[2]] <- 0L } out[op] <- basis64[1 + bitwShiftR(c[[1]], 2L)] op <- op + 1L out[op] <- basis64[1 + bitwOr(bitwShiftL(bitwAnd(c[[1]], 3L), 4L), bitwShiftR(bitwAnd(c[[2]], 240L), 4L))] op <- op + 1L if (len > 2) { c[[3]] <- as.integer(x[ip]) ip <- ip + 1L out[op] <- basis64[1 + bitwOr(bitwShiftL(bitwAnd(c[[2]], 15L), 2L), bitwShiftR(bitwAnd(c[[3]], 192L), 6L))] op <- op + 1L out[op] <- basis64[1 + bitwAnd(c[[3]], 63)] op <- op + 1L } else if (len == 2) { out[op] <- basis64[1 + bitwShiftL(bitwAnd(c[[2]], 15L), 2L)] op <- op + 1L out[op] <- charToRaw("=") op <- op + 1L } else { ## len == 1 out[op] <- charToRaw("=") op <- op + 1L out[op] <- charToRaw("=") op <- op + 1L } len <- len - 3L } rawToChar(out) } webfakes/R/compat-defer.R0000644000176200001440000000370214172041777014747 0ustar liggesusers# nocov start --- compat-defer --- 2020-06-16 # This drop-in file implements withr::defer(). Please find the most # recent version in withr's repository. defer <- function(expr, envir = parent.frame(), priority = c("first", "last")) { } local({ defer <<- defer <- function(expr, envir = parent.frame(), priority = c("first", "last")) { priority <- match.arg(priority) if (identical(envir, .GlobalEnv) && is.null(get_handlers(envir))) { message( "Setting deferred event(s) on global environment.\n", " * Execute (and clear) with `withr::deferred_run()`.\n", " * Clear (without executing) with `withr::deferred_clear()`." ) } invisible( add_handler( envir, handler = list(expr = substitute(expr), envir = parent.frame()), front = priority == "first" ) ) } get_handlers <- function(envir) { attr(envir, "handlers") } set_handlers <- function(envir, handlers) { has_handlers <- "handlers" %in% names(attributes(envir)) attr(envir, "handlers") <- handlers if (!has_handlers) { call <- make_call(execute_handlers, envir) # We have to use do.call here instead of eval because of the way on.exit # determines its evaluation context # (https://stat.ethz.ch/pipermail/r-devel/2013-November/067867.html) do.call(base::on.exit, list(call, TRUE), envir = envir) } } execute_handlers <- function(envir) { handlers <- get_handlers(envir) errors <- list() for (handler in handlers) { tryCatch(eval(handler$expr, handler$envir), error = function(e) { errors[[length(errors) + 1]] <<- e } ) } for (error in errors) { stop(error) } } add_handler <- function(envir, handler, front) { if (front) { handlers <- c(list(handler), get_handlers(envir)) } else { handlers <- c(get_handlers(envir), list(handler)) } set_handlers(envir, handlers) handler } make_call <- function(...) { as.call(list(...)) } }) # defer() namespace # nocov end webfakes/R/mw-multipart.R0000644000176200001440000000724614172041777015052 0ustar liggesusers #' Parse a multipart HTTP request body #' #' Adds the parsed form fields in the `form` element of the request and #' the parsed files to the `files` element. #' #' @param type Content type to match before parsing. If it does not #' match, then the request object is not modified. #' @return Handler function. #' #' @family middleware #' @export #' @examples #' app <- new_app() #' app$use(mw_multipart()) #' app mw_multipart <- function(type = "multipart/form-data") { type function(req, res) { ct <- req$get_header("Content-Type") %||% "" if (!any(vapply( paste0("^", type), function(x) grepl(x, ct), logical(1)))) return("next") parts <- str_trim(strsplit(ct, ";", fixed = TRUE)[[1]]) bnd <- grep("boundary=", parts, value = TRUE)[1] if (is.na(bnd)) return("next") bnd <- sub("^boundary=", "", bnd) tryCatch({ mp <- parse_multipart(req$.body, bnd) req$form <- list() req$files <- list() for (p in mp) { if (is.null(p$filename)) { req$form[[p$name]] <- rawToChar(p$value) } else { req$files[[p$name]] <- list(filename = p$filename, value = p$value) } } }, error = function(err) NULL) "next" } } parse_multipart <- function(body, boundary) { boundary <- paste0("--", boundary) boundary_length <- nchar(boundary) # Find the locations of the boundary string indexes <- grepRaw(boundary, body, fixed = TRUE, all = TRUE) if (!length(indexes)) stop("Boundary was not found in the body.") if (length(indexes) == 1) { if (length(body) < (boundary_length + 5)) { # Empty HTML5 FormData object return(list()) } else { # Something went wrong stop("The 'boundary' was only found once in the ", "multipart/form-data message. It should appear at ", "least twice. The request-body might be truncated.") } } parts <- list() for (i in seq_along(utils::head(indexes, -1))) { from <- indexes[i] + boundary_length to <- indexes[i + 1] -1 parts[[i]] <- body[from:to] } out <- lapply(parts, multipart_sub) names(out) <- vapply( out, function(x) as.character(x$name), character(1) ) out } multipart_sub <- function(bodydata) { splitchar <- grepRaw("\\r\\n\\r\\n|\\n\\n|\\r\\r", bodydata) if (!length(splitchar)) { stop("Invalid multipart subpart:\n\n", rawToChar(bodydata)) } headers <- bodydata[1:(splitchar-1)] headers <- str_trim(rawToChar(headers)) headers <- gsub("\r\n", "\n", headers) headers <- gsub("\r", "\n", headers) headerlist <- unlist(lapply(strsplit(headers, "\n")[[1]], str_trim)) dispindex <- grep("^Content-Disposition:", headerlist) if (!length(dispindex)) { stop("Content-Disposition header not found:", headers) } dispheader <- headerlist[dispindex] #get parameter name m <- regexpr("; name=\\\"(.*?)\\\"", dispheader) if (m < 0) stop('failed to find the name="..." header') namefield <- unquote(sub( "; name=", "", regmatches(dispheader, m), fixed = TRUE )) #test for file upload m <- regexpr("; filename=\\\"(.*?)\\\"", dispheader) if (m < 0) { filenamefield = NULL } else { filenamefield <- unquote(sub( "; filename=", "", regmatches(dispheader, m), fixed = TRUE )) } #filedata splitval <- grepRaw("\\r\\n\\r\\n|\\n\\n|\\r\\r", bodydata, value=TRUE) start <- splitchar + length(splitval) if (identical(utils::tail(bodydata, 2), charToRaw("\r\n"))) { end <- length(bodydata) - 2 } else { end <- length(bodydata) - 1 } #the actual fields list ( name = namefield, value = bodydata[start:end], filename = filenamefield ) } webfakes/R/tmpl-glue.R0000644000176200001440000000337714740243712014311 0ustar liggesusers #' glue based template engine #' #' Use this template engine to create pages with glue templates. #' See [glue::glue()] for the syntax. #' #' @param sep Separator used to separate elements. #' @param open The opening delimiter. Doubling the full delimiter escapes #' it. #' @param close The closing delimiter. Doubling the full delimiter escapes #' it. #' @param na Value to replace NA values with. If `NULL` missing values are #' propagated, that is an `NA` result will cause `NA` output. #' Otherwise the value is replaced by the value of `na`. #' @param transformer A function taking three parameters `code`, `envir` #' and `data` used to transform the output of each block before during or #' after evaluation. #' @param trim Whether to trim the input template with [glue::trim()] or not. #' @return Template function. #' #' @export #' @examples #' # See th 'hello' app at #' hello_root <- system.file(package = "webfakes", "examples", "hello") #' hello_root #' #' app <- new_app() #' app$engine("txt", tmpl_glue()) #' app$use(mw_log()) #' #' #' app$get("/view", function(req, res) { #' txt <- res$render("test") #' res$ #' set_type("text/plain")$ #' send(txt) #' }) #' #' # Switch to the app's root: setwd(hello_root) #' # Now start the app with: app$listen(3000L) #' # Or start it in another process: new_process(app) tmpl_glue <- function(sep = "", open = "{", close = "}", na = "NA", transformer = NULL, trim = TRUE) { sep; open; close; na; transformer; trim function(path, locals) { txt <- readChar(path, nchars = file.size(path)) glue::glue_data( locals, txt, .sep = sep, .open = open, .close = close, .na = na, .transformer = transformer %||% glue::identity_transformer, .trim = trim ) } } webfakes/R/webfakes-package.R0000644000176200001440000000013514370706367015561 0ustar liggesusers#' @keywords internal "_PACKAGE" ## usethis namespace: start ## usethis namespace: end NULL webfakes/R/mw-json.R0000644000176200001440000000177314172041777014001 0ustar liggesusers #' Middleware to parse a JSON body #' #' Adds the parsed object as the `json` element of the request object. #' #' @param type Content type to match before parsing. If it does not #' match, then the request object is not modified. #' @param simplifyVector Whether to simplify lists to vectors, passed to #' [jsonlite::fromJSON()]. #' @param ... Arguments to pass to [jsonlite::fromJSON()], that performs #' the JSON parsing. #' @return Handler function. #' #' @family middleware #' @export #' @examples #' app <- new_app() #' app$use(mw_json()) #' app mw_json <- function(type = "application/json", simplifyVector = FALSE, ...) { type; simplifyVector; list(...) function(req, res) { ct <- req$get_header("Content-Type") %||% "" if (! ct %in% tolower(type)) return("next") if (!is.null(req$.body)) { req$json <- jsonlite::fromJSON( rawToChar(req$.body), simplifyVector = simplifyVector, ... ) } "next" } } webfakes/R/mw-static.R0000644000176200001440000000237114310055210014267 0ustar liggesusers #' Middleware function to serve static files #' #' The content type of the response is set automatically from the #' extension of the file. Note that this is a terminal middleware #' handler function. If a file is served, then the rest of the handler #' functions will not be called. If a file was not found, however, #' the rest of the handlers are still called. #' #' @param root Root path of the served files. Everything under this #' directory is served automatically. Directory lists are not currently #' supports. #' @param set_headers Callback function to call before a file is served. #' @return Handler function. #' #' @family middleware #' @export #' @examples #' root <- system.file(package = "webfakes", "examples", "static", "public") #' app <- new_app() #' app$use(mw_static(root = root)) #' app mw_static <- function(root, set_headers = NULL) { root; set_headers function(req, res) { path <- file.path(root, sub("^/", "", req$path)) if (!file.exists(path)) return("next") if (file.info(path)$isdir) return("next") ext <- tools::file_ext(basename(path)) ct <- mime_find(ext) if (!is.na(ct)) { res$set_header("Content-Type", ct) } if (!is.null(set_headers)) set_headers(req, res) res$send(read_bin(path)) } } webfakes/R/package.R0000644000176200001440000000010214172041777013763 0ustar liggesusers #' @useDynLib webfakes, .registration = TRUE, .fixes = "c_" NULL webfakes/R/mw-raw.R0000644000176200001440000000110114172041777013602 0ustar liggesusers #' Middleware to read the raw body of a request #' #' Adds the raw body, as a raw object to the `raw` field of the request. #' #' @param type Content type to match. If it does not match, then the #' request object is not modified. #' @return Handler function. #' #' @family middleware #' @export #' @examples #' app <- new_app() #' app$use(mw_raw()) #' app mw_raw <- function(type = "application/octet-stream") { function(req, res) { ct <- req$get_header("Content-Type") %||% "" if (! ct %in% tolower(type)) return("next") req$raw <- req$.body "next" } } webfakes/R/mw-text.R0000644000176200001440000000137514172041777014012 0ustar liggesusers #' Middleware to parse a plain text body #' #' Adds the parsed object as the `text` element of the request object. #' #' @param default_charset Encoding to set on the text. #' @param type Content type to match before parsing. If it does not #' match, then the request object is not modified. #' @return Handler function. #' #' @family middleware #' @export #' @examples #' app <- new_app() #' app$use(mw_text()) #' app mw_text <- function(default_charset = "utf-8", type = "text/plain") { default_charset; type function(req, res) { ct <- req$get_header("Content-Type") %||% "" if (! ct %in% tolower(type)) return("next") req$text <- rawToChar(req$.body %||% raw()) Encoding(req$text) <- default_charset "next" } } webfakes/R/mw-log.R0000644000176200001440000000331614172041777013604 0ustar liggesusers #' Log requests to the standard output or other connection #' #' A one line log entry for every request. The output looks like this: #' ``` #' GET http://127.0.0.1:3000/image 200 3 ms - 4742 #' ``` #' and contains #' * the HTTP method, #' * the full request URL, #' * the HTTP status code of the response, #' * how long it took to process the response, in ms, #' * and the size of the response body, in bytes. #' #' @param format Log format. Not implemented currently. #' @param stream R connection to log to. `"stdout"` means the standard #' output, `"stderr"` is the standard error. You can also supply a #' connection object, but then you need to be sure that it will be #' valid when the app is actually running. #' @return Handler function. #' #' @family middleware #' @export #' @examples #' app <- new_app() #' app$use(mw_log()) #' app mw_log <- function(format = "dev", stream = "stdout") { format; stream function(req, res) { start <- Sys.time() fmt <- function(req, res) { if (identical(stream, "stdout")) stream <- stdout() if (identical(stream, "stderr")) stream <- stderr() len <- if (is.null(res$.body)) { 0L # nocov } else if (is.raw(res$.body)) { length(res$.body) } else if (is_string(res$.body)) { nchar(res$.body, type = "bytes") } else { "??" # nocov } t <- as.integer(round((Sys.time() - start) * 1000)) msg <- sprintf( "%s %s %s %s ms - %s\n", toupper(req$method), req$url, res$.status, t, len ) cat0(msg, file = stream) if (inherits(stream, "connection")) flush(stream) } res$on_response(fmt) "next" } } webfakes/R/utils.R0000644000176200001440000000752614740243712013543 0ustar liggesusers `%||%` <- function(l, r) if (is.null(l)) r else l `%|NA|%` <- function(l, r) if (is_na_scalar(l)) r else l new_object <- function(class_name, ...) { structure(as.environment(list(...)), class = class_name) } is_string <- function(x) { is.character(x) && length(x) == 1 && !is.na(x) } is_integerish <- function(x) { is.integer(x) || (is.numeric(x) && all(round(x) == x)) } is_count <- function(x) { is_integerish(x) && length(x) == 1 && !is.na(x) } is_port <- function(x) { is_count(x) } is_na_scalar <- function(x) { is.atomic(x) && length(x) == 1 && is.na(x) } cat0 <- function(..., sep = "") { cat(..., sep = sep, append = TRUE) } str_trim <- function(x) { sub("\\s+$", "", sub("^\\s+", "", x)) } unquote <- function(str) { len <- nchar(str) if (substr(str, 1, 1) == '"' && substr(str, len, len) == '"') { substr(str, 2, len - 1) } else { str } } isFALSE <- function(x) { identical(x, FALSE) } str_is_suffix <- function(x, sfx) { lx <- nchar(x) lsfx <- nchar(sfx) substring(x, lx - lsfx + 1, lx) == sfx } read_char <- function(path, encoding = "UTF-8") { txt <- rawToChar(readBin(path, "raw", file.info(path)$size)) Encoding(txt) <- encoding txt } read_bin <- function(path) { readBin(path, "raw", file.info(path)$size) } try_silently <- function(expr) { try(expr, silent = TRUE) } sseq <- function(from, to) { if (from > to) integer() else seq(from, to) } is.named <- function(x) { length(names(x)) == length(x) && all(names(x) != "") } set_envvar <- function(envs) { if (length(envs) == 0) return() stopifnot(is.named(envs)) old <- Sys.getenv(names(envs), names = TRUE, unset = NA) set <- !is.na(envs) both_set <- set & !is.na(old) if (any(set)) do.call("Sys.setenv", as.list(envs[set])) if (any(!set)) Sys.unsetenv(names(envs)[!set]) invisible(old) } mkdirp <- function(path, ...) { dir.create(path, showWarnings = FALSE, recursive = TRUE, ...) } strrep <- function(x, no) { paste(rep(x, no), collapse = "") } set_name <- function(x, nm) { names(x) <- nm x } random_id <- generate_token <- function(n = 30) { paste0(sample(c(0:9, letters[1:6]), n, replace = TRUE), collapse = "") } parse_url <- function(url) { re_url <- paste0( "^(?[a-zA-Z0-9]+)://", "(?:(?[^@/:]+)(?::(?[^@/]+))?@)?", "(?[^/]+)", "(?.*)$" # don't worry about query params here... ) re_match(url, re_url)$groups } modify_path <- function(url, path) { purl <- parse_url(url) has_usr <- nzchar(purl$username) has_pwd <- nzchar(purl$password) paste0( purl$protocol, "://", purl$username, if (has_pwd) ":", purl$password, if (has_usr) "@", purl$host, if (substr(path, 1, 1) != "/") "/", path ) } has_seed <- function() { exists(".Random.seed", globalenv(), mode = "integer", inherits = FALSE) } get_seed <- function() { if (has_seed()) { get(".Random.seed", globalenv(), mode = "integer", inherits = FALSE) } } set_seed <- function(seed) { if (is.null(seed)) { rm(".Random.seed", envir = globalenv()) } else { assign(".Random.seed", seed, globalenv()) } } local_options <- function(.new = list(), ..., .local_envir = parent.frame()) { .new <- utils::modifyList(as.list(.new), list(...)) old <- do.call(options, .new) defer(do.call(options, old), .local_envir) } map_chr <- function(X, FUN, ...) { vapply(X, FUN, FUN.VALUE = character(1), ...) } #' Format a time stamp for HTTP #' #' @param t Date-time value to format, defaults to the current date and #' time. It must be a POSIXct object. #' @return Character vector, formatted date-time. #' @export http_time_stamp <- function(t = Sys.time()) { t <- as.POSIXlt(t, tz = "UTC") strftime(t, "%a, %d %b %Y %H:%M:%S GMT") } capitalize <- function(x) { substr(x, 1, 1) <- toupper(substr(x, 1, 1)) x } webfakes/R/mw-authorization.R0000644000176200001440000000434114740243712015714 0ustar liggesusers parse_authorization_header <- function(x) { if (length(x) == 0) return(NULL) scheme <- tolower(sub("[ ].*$", "", x)) rest <- trimws(sub("^[^ ]+[ ]", "", x)) if (scheme == "basic") { username <- password <- NULL tryCatch({ ptxt <- strsplit(base64_decode(rest), ":", fixed = TRUE)[[1]] if (length(ptxt) == 2) { username <- ptxt[1] password <- ptxt[2] } }, error = function(err) NULL) if (!is.null(username) && !is.null(password)) { return(list( scheme = scheme, username = username, password = password )) } else { return(NULL) } } # if it has a (non-trailing) =, then it is a dictionary, otherwise token if (grepl("=", sub("=+$", "", rest))) { return(c(list(scheme = scheme), parse_dict_header(rest))) } list(scheme = scheme, token = rest) } parse_dict_header <- function(x) { result <- list() for (item in parse_list_header(x)) { if (!grepl("=", item)) { result[item] <- list(NULL) next } key <- sub("=.*$", "", item) value <- sub("^[^=]+=", "", item) # https://www.rfc-editor.org/rfc/rfc2231#section-4 # we always assume UTF-8 if (grepl("[*]$", key)) { key <- sub("[*]$", "", key) value <- sub("^.*'.*'", "", value) value <- utils::URLdecode(value) Encoding(value) <- "UTF-8" } if (grepl('^".*"$', value)) { value <- substr(value, 2, nchar(value) - 1L) } result[[key]] <- value } result } parse_list_header <- function(x) { s <- strsplit(x, "")[[1]] res <- character() part <- character() escape <- quote <- FALSE for (cur in s) { if (escape) { part[length(part) + 1L] <- cur escape <- FALSE next } if (quote) { if (cur == "\\") { escape <- TRUE next } else if (cur == '"') { quote <- FALSE } part[length(part) + 1L] <- cur next } if (cur == ",") { res[length(res) + 1L] <- paste(part, collapse = "") part <- character() next } if (cur == '"') { quote <- TRUE } part[length(part) + 1L] <- cur } if (length(part)) { res[length(res) + 1L] <- paste(part, collapse = "") } trimws(res) } webfakes/R/response.R0000644000176200001440000003355214740243712014237 0ustar liggesusers #' A webfakes response object #' #' webfakes creates a `webfakes_response` object for every incoming HTTP #' request. This object is passed to every matched route and middleware, #' until the HTTP response is sent. It has reference semantics, so handlers #' can modify it. #' #' Fields and methods: #' #' * `app`: The `webfakes_app` object itself. #' * `req`: The request object. #' * `headers_sent`: Whether the response headers were already sent out. #' * `locals`: Local variables, the are shared between the handler #' functions. This is for the end user, and not for the middlewares. #' * `delay(secs)`: delay the response for a number of seconds. If a #' handler calls `delay()`, the same handler will be called again, #' after the specified number of seconds have passed. Use the `locals` #' environment to distinguish between the calls. If you are using #' `delay()`, and want to serve requests in parallel, then you probably #' need a multi-threaded server, see [server_opts()]. #' * `add_header(field, value)`: Add a response header. Note that #' `add_header()` may create duplicate headers. You usually want #' `set_header()`. #' * `get_header(field)`: Query the currently set response headers. If #' `field` is not present it return `NULL`. #' * `on_response(fun)`: Run the `fun` handler function just before the #' response is sent out. At this point the headers and the body are #' already properly set. #' * `redirect(path, status = 302)`: Send a redirect response. It sets #' the `Location` header, and also sends a `text/plain` body. #' * `render(view, locals = list())`: Render a template page. Searches #' for the `view` template page, using all registered engine extensions, #' and calls the first matching template engine. Returns the filled #' template. #' * `send(body)`. Send the specified body. `body` can be a raw vector, #' or HTML or other text. For raw vectors it sets the content type to #' `application/octet-stream`. #' * `send_json(object = NULL, text = NULL, ...)`: Send a JSON response. #' Either `object` or `text` must be given. `object` will be converted #' to JSON using [jsonlite::toJSON()]. `...` are passed to #' [jsonlite::toJSON()]. It sets the content type appropriately. #' * `send_file(path, root = ".")`: Send a file. Set `root = "/"` for #' absolute file names. It sets the content type automatically, based #' on the extension of the file, if it is not set already. #' * `send_status(status)`: Send the specified HTTP status code, without #' a response body. #' * `send_chunk(data)`: Send a chunk of a response in chunked encoding. #' The first chunk will automatically send the HTTP response headers. #' Webfakes will automatically send a final zero-lengh chunk, unless #' `$delay()` is called. #' * `set_header(field, value)`: Set a response header. If the headers have #' been sent out already, then it throws a warning, and does nothing. #' * `set_status(status)`: Set the response status code. If the headers #' have been sent out already, then it throws a warning, and does nothing. #' * `set_type(type)`: Set the response content type. If it contains a `/` #' character then it is set as is, otherwise it is assumed to be a file #' extension, and the corresponding MIME type is set. If the headers have #' been sent out already, then it throws a warning, and does nothing. #' * `add_cookie(name, value, options)`: Adds a cookie to the response. #' `options` is a named list, and may contain: #' * `domain`: Domain name for the cookie, not set by default. #' * `expires`: Expiry date in GMT. It must be a POSIXct object, and #' will be formatted correctly. #' * 'http_only': if TRUE, then it sets the 'HttpOnly' attribute, so #' Javasctipt cannot access the cookie. #' * `max_age`: Maximum age, in number of seconds. #' * `path`: Path for the cookie, defaults to "/". #' * `same_site`: The 'SameSite' cookie attribute. Possible values are #' "strict", "lax" and "none". #' * `secure`: if TRUE, then it sets the 'Secure' attribute. #' * `clear_cookie(name, options = list())`: clears a cookie. Typically, #' web browsers will only clear a cookie if the options also match. #' * `write(data)`: writes (part of) the body of the response. It also #' sends out the response headers, if they haven't been sent out before. #' #' Usually you need one of the `send()` methods, to send out the HTTP #' response in one go, first the headers, then the body. #' #' Alternatively, you can use `$write()` to send the response in parts. #' #' @seealso [webfakes_request] for the webfakes request object. #' @name webfakes_response #' @examples #' # This is how you can see the request and response objects: #' app <- new_app() #' app$get("/", function(req, res) { #' browser() #' res$send("done") #' }) #' app #' #' # Now start this app on a port: #' # app$listen(3000) #' # and connect to it from a web browser: http://127.0.0.1:3000 #' # You can also use another R session to connect: #' # httr::GET("http://127.0.0.1:3000") #' # or the command line curl tool: #' # curl -v http://127.0.0.1:3000 #' # The app will stop while processing the request. NULL new_response <- function(app, req) { self <- new_object( "webfakes_response", app = app, req = req, locals = as.environment(as.list(app$locals)), headers_sent = FALSE, delay = function(secs) { self$.stackptr <- self$.i self$.delay <- secs response_delay(self$req, secs) invisible(NULL) }, get_header = function(field) { # this is case insensitive h <- self$.headers names(h) <- tolower(names(h)) h[[tolower(field)]] }, on_response = function(fun) { self$.on_response <- c(self$.on_response, list(fun)) invisible(self) }, redirect = function(path, status = 302) { if (self$.check_sent()) return(invisible(self)) self$ set_status(status)$ set_header("Location", path)$ set_type("text/plain")$ send(paste0( status, " ", http_statuses[as.character(status)], ". Redirecting to ", path )) invisible(self) }, render = function(view, locals = list()) { locals <- as.environment(as.list(locals)) parent.env(locals) <- self$locals root <- self$app$get_config("views") for (eng in self$app$.engines) { f <- file.path(root, paste0(view, ".", eng$ext)) if (file.exists(f)) return(eng$engine(f, locals)) } stop("Cannot find template engine for view '", view, "'") }, send = function(body) { if (self$.check_sent()) return(invisible(self)) # We need to do these here, on_response middleware might depend on it self$.body <- body self$.set_defaults() for (fn in self$.on_response) fn(self$req, self) response_send(self$req) self$headers_sent <- TRUE self$.sent <- TRUE invisible(self) }, send_chunk = function(data) { if (self$.check_sent()) return(invisible(self)) # The first chunk sends the headers automatically, but we make # sure to set chunked encoding if (! self$headers_sent) { self$set_header("Transfer-Encoding", "chunked") if (is.null(self$get_header("Content-Type"))) { self$set_header("Content-Type", "application/octet-stream") } if (is.null(self$.status)) self$set_status(200L) self$.set_defaults() } enc <- self$get_header("Transfer-Encoding") if (enc != "chunked") { warning("Headers sent, cannot set chunked encoding now") return(invisible(self)) } if (is.character(data)) data <- charToRaw(paste(data, collapse = "\n")) response_send_chunk(self$req, data) self$headers_sent <- TRUE invisible(self) }, send_json = function(object = NULL, text = NULL, ...) { if (!is.null(object) && !is.null(text)) { stop("Specify only one of `object` and `text` in `send_json()`") } if (is.null(text)) { text <- jsonlite::toJSON(object, ...) } self$ set_header("Content-Type", "application/json")$ send(text) }, send_file = function(path, root = ".") { # Set content type automatically if (is.null(self$get_header("Content-Type"))) { ext <- tools::file_ext(basename(path)) ct <- mime_find(ext) if (!is.na(ct)) { self$set_header("Content-Type", ct) } } if (root == "/" && .Platform$OS.type == "windows" && grepl("^[a-zA-Z]:", path)) { abs_path <- path } else { abs_path <- file.path(root, path) } self$send(read_bin(normalizePath(abs_path))) }, send_status = function(status) { self$ set_status(status)$ send("") }, set_header = function(field, value) { if (self$.check_sent()) return(invisible(self)) self$.headers[[field]] <- as.character(value) invisible(self) }, add_header = function(field, value) { if (self$.check_sent()) return(invisible(self)) h <- structure(list(value), names = field) self$.headers <- append(self$.headers, h) invisible(self) }, set_status = function(status) { if (self$.check_sent()) return(invisible(self)) self$.status <- as.integer(status) invisible(self) }, set_type = function(type) { if (self$.check_sent()) return(invisible(self)) if (grepl("/", type)) { self$set_header("Content-Type", type) } else { ct <- mime_find(type) if (!is.na(ct)) { self$set_header("Content-Type", ct) } } invisible(self) }, add_cookie = function(name, value, options = list()) { if (!is_string(name)) { stop("Cookie name must be a string.") } if (grepl("[=;]", name)) { stop("Cookie name cannot contain ';' and '=' characters.") } if (!is_string(value)) { stop("Cookie value must be a string.") } if (grepl("[=;]", value)) { stop("Cookie value cannot contain ';' and '=' characters.") } ck <- paste0( name, "=", value, "; ", format_cookie_options(options) ) self$add_header("Set-Cookie", ck) invisible(self) }, clear_cookie = function(name, options = list()) { if (!is_string(name)) { stop("Cookie name must be a string.") } if (grepl("[=;]", name)) { stop("Cookie name cannot contain ';' and '=' characters.") } options$expires <- .POSIXct(0) options$max_age <- 0L ck <- paste0(name, "=; ", format_cookie_options(options)) self$add_header("Set-Cookie", ck) invisible(self) }, write = function(data) { if (is.null(self$get_header("content-length"))) { warning("response$write() without a Content-Length header") } if (is.null(self$.status)) self$set_status(200L) if (is.character(data)) data <- charToRaw(paste(data, collapse = "\n")) response_write(self$req, data) self$headers_sent <- TRUE invisible(self) }, .check_sent = function() { if (isTRUE(self$.sent)) { warning("Response is sent already") } self$.sent }, .set_defaults = function() { if (is.null(self$.status)) { if (is.null(self$.body)) { # No status, no body, that's 404 self$.status <- 404L self$.body <- "Not found" } else { # No status, but body, set status self$.status <- 200L } } # Set Content-Type if not set if (is.null(self$get_header("Content-Type"))) { if (is.raw(self$body)) { ct <- "application/octet-stream" } else { ct <- "text/plain" } self$set_header("Content-Type", ct) } # Set Content-Length if not set if (is.null(self$get_header("Content-Length")) && (self$get_header("Transfer-Encoding") %||% "") != "chunked") { if (is.raw(self$.body)) { cl <- length(self$.body) } else if (is.character(self$.body)) { cl <- nchar(self$.body, type = "bytes") } else if (is.null(self$.body)) { cl <- 0L } self$set_header("Content-Length", cl) } # Make sure response to HEAD has empty body if (self$req$method == "head") { self$.body <- raw(0) self$set_header("Content-Length", "0") } }, .body = NULL, .status = NULL, .headers = if (!app$.enable_keep_alive) list("Connection" = "close") else list(), .on_response = NULL, .sent = FALSE, .stackptr = 1L ) self } format_cookie_options <- function(options) { options$path <- options$path %||% "/" bad <- unique(setdiff( names(options), c("domain", "expires", "http_only", "max_age", "path", "same_site", "secure") )) if (length(bad)) { stop( "Unknown or unsupported cookie attribute(s): ", paste0("\"", bad, "\"", collapse = ", "), "." ) } parts <- c( if (!is.null(options$domain)) { paste0("Domain=", options$domain) }, if (!is.null(options$expires)) { if (!inherits(options$expires, "POSIXct")) { stop("The 'expires' cookie attribute must be a POSIXct object") } paste0("Expires=", http_time_stamp(options$expires)) }, if (isTRUE(options$http_only)) { "HttpOnly" }, if (!is.null(options$max_age)) { paste0("Max-Age=", options$max_age) }, paste0("Path=", options$path), if (!is.null(options$same_site)) { if (tolower(!options$same_site) %in% c("strict", "lax", "none")) { stop( "Invalid value for 'SameSite' cookie atrribute: ", options$same_site, ", must be \"strict\", \"lax\" or \"none\"." ) } paste0("SameSite=", capitalize(options$same_site)) }, if (isTRUE(options$secure)) { "Secure" } ) paste(parts, collapse = "; ") } webfakes/R/status.R0000644000176200001440000000501714172041777013725 0ustar liggesusers http_statuses <- c( "100" = "Continue", "101" = "Switching Protocols", "102" = "Processing (WebDAV; RFC 2518)", "200" = "OK", "201" = "Created", "202" = "Accepted", "203" = "Non-Authoritative Information", "204" = "No Content", "205" = "Reset Content", "206" = "Partial Content", "207" = "Multi-Status (WebDAV; RFC 4918)", "208" = "Already Reported (WebDAV; RFC 5842)", "226" = "IM Used (RFC 3229)", "300" = "Multiple Choices", "301" = "Moved Permanently", "302" = "Found", "303" = "See Other", "304" = "Not Modified", "305" = "Use Proxy", "306" = "Switch Proxy", "307" = "Temporary Redirect", "308" = "Permanent Redirect (experimental Internet-Draft)", "400" = "Bad Request", "401" = "Unauthorized", "402" = "Payment Required", "403" = "Forbidden", "404" = "Not Found", "405" = "Method Not Allowed", "406" = "Not Acceptable", "407" = "Proxy Authentication Required", "408" = "Request Timeout", "409" = "Conflict", "410" = "Gone", "411" = "Length Required", "412" = "Precondition Failed", "413" = "Request Entity Too Large", "414" = "Request-URI Too Long", "415" = "Unsupported Media Type", "416" = "Requested Range Not Satisfiable", "417" = "Expectation Failed", "418" = "I'm a teapot (RFC 2324)", "420" = "Enhance Your Calm (Twitter)", "422" = "Unprocessable Entity (WebDAV; RFC 4918)", "423" = "Locked (WebDAV; RFC 4918)", "424" = "Failed Dependency (WebDAV; RFC 4918)", "424" = "Method Failure (WebDAV)", "425" = "Unordered Collection (Internet draft)", "426" = "Upgrade Required (RFC 2817)", "428" = "Precondition Required (RFC 6585)", "429" = "Too Many Requests (RFC 6585)", "431" = "Request Header Fields Too Large (RFC 6585)", "444" = "No Response (Nginx)", "449" = "Retry With (Microsoft)", "450" = "Blocked by Windows Parental Controls (Microsoft)", "451" = "Unavailable For Legal Reasons (Internet draft)", "499" = "Client Closed Request (Nginx)", "500" = "Internal Server Error", "501" = "Not Implemented", "502" = "Bad Gateway", "503" = "Service Unavailable", "504" = "Gateway Timeout", "505" = "HTTP Version Not Supported", "506" = "Variant Also Negotiates (RFC 2295)", "507" = "Insufficient Storage (WebDAV; RFC 4918)", "508" = "Loop Detected (WebDAV; RFC 5842)", "509" = "Bandwidth Limit Exceeded (Apache bw/limited extension)", "510" = "Not Extended (RFC 2774)", "511" = "Network Authentication Required (RFC 6585)", "598" = "Network read timeout error (Unknown)", "599" = "Network connect timeout error (Unknown)" ) webfakes/R/oauth.R0000644000176200001440000005374114172041777013531 0ustar liggesusers #' Fake OAuth 2.0 resource and authorization app #' #' @includeRmd man/rmd-fragments/oauth2.Rmd description #' #' @details #' The app has the following endpoints: #' * `GET /register` is the endpoint that you can use to register #' your third party app. It needs to receive the `name` of the #' third party app, and its `redirect_uri` as query parameters, #' otherwise returns an HTTP 400 error. On success it returns a #' JSON dictionary with entries `name` (the name of the third party #' app), `client_id`, `client_secret` and `redirect_uri`. #' * `GET /authorize` is the endpoint where the user of the third #' party app is sent. You can change the URL of this endpoint with #' the `authorize_endpoint` argument. It needs to receive the `client_id` #' of the third party app, and its correct `redirect_uri` as query #' parameters. It may receive a `state` string as well, which can #' be used by a client to identify the request. Otherwise it #' generates a random `state` string. On error it fails with a HTTP #' 400 error. On success it returns a simple HTML login page. #' * `POST /authorize/decision` is the endpoint where the HTML login #' page generated at `/authorize` connects back to, either with a #' positive or negative result. The form on the login page will send #' the `state` string and the user's choice in the `action` variable. #' If the user authorized the third party app, then they are #' redirected to the `redirect_uri` of the app, with a temporary #' `code` and the `state` string supplied as query parameters. #' Otherwise a simple HTML page is returned. #' * `POST /token` is the endpoint where the third party app requests #' a temporary access token. It is also uses for refreshing an #' access token with a refresh token. You can change the URL of this #' endpoint with the `token_endpoint` argument. #' To request a new token or refresh an existing one, the following #' data must be included in either a JSON or an URL encoded request body: #' - `grant_type`, this must be `authorization_code` for new tokens, #' and `refresh_token` for refreshing. #' - `code`, this must be the temporary code obtained from the #' `/authorize/decision` redirection, for new tokens. It is not #' needed when refreshing. #' - `client_id` must be the client id of the third party app. #' - `client_secret` must be the client secret of the third party #' app. #' - `redirect_uri` must be the correct redirection URI of the #' third party app. It is not needed when refreshing tokens. #' - `refresh_token` must be the refresh token obtained previously, #' when refreshing a token. It is not needed for new tokens. #' On success a JSON dictionary is returned with entries: #' `access_token`, `expiry` and `refresh_token`. (The latter is #' omitted if the `refresh` argument is `FALSE`). #' * `GET /locals` returns a list of current apps, access tokens and #' refresh tokens. #' * `GET /data` is an endpoint that returns a simple JSON response, #' and needs authorization. #' #' ## Notes #' #' * Using this app in your tests requires the glue package, so you #' need to put it in `Suggests`. #' * You can add custom endpoints to the app, as needed. #' * If you need authorization in your custom endpoint, call #' `app$is_authorized()` in your handler: #' ``` #' if (!app$is_authorized(req, res)) return() #' ``` #' `app$is_authorized()` returns an HTTP 401 response if the #' client is not authorized, so you can simply return from your #' handler. #' #' For more details see `vignette("oauth", package = "webfakes")`. #' #' @section `oauth2_resource_app()`: #' App representing the API server (resource/authorization) #' @return a `webfakes` app #' @param access_duration After how many seconds should access tokens #' expire. #' @param refresh_duration After how many seconds should refresh #' tokens expire (ignored if `refresh` is `FALSE`). #' @param refresh Should a refresh token be returned (logical). #' @param seed Random seed used when creating tokens. If `NULL`, #' we rely on R to provide a seed. The app uses its own RNG stream, #' so it does not affect reproducibility of the tests. #' @param authorize_endpoint The authorization endpoint of the resource #' server. Change this from the default if the real app that you #' are faking does not use `/authorize`. #' @param token_endpoint The endpoint to request tokens. Change this if the #' real app that you are faking does not use `/token`. #' @return webfakes app #' #' @export #' @family OAuth2.0 functions oauth2_resource_app <- function(access_duration = 3600L, refresh_duration = 7200L, refresh = TRUE, seed = NULL, authorize_endpoint = "/authorize", token_endpoint = "/token") { access_duration refresh_duration refresh seed authorize_endpoint token_endpoint app <- new_app() app$locals$seed <- seed %||% get_seed() # Parse body for /authorize/decision, /token app$use(mw_urlencoded()) app$use(mw_json()) app$locals$tpapps <- data.frame( name = character(), client_id = character(), client_secret = character(), redirect_uri = character() ) app$set_config("views", system.file("views", package = "webfakes")) app$engine("html", tmpl_glue()) app$get("/register", function(req, res) { if (is.null(req$query$name) || is.null(req$query$redirect_uri)) { res$ set_status(400L)$ send("Cannot register without 'name' and 'redirect_uri'") return() } rec <- list( name = req$query$name, client_id = paste0("id-", generate_token()), client_secret = paste0("secret-", generate_token()), redirect_uri = req$query$redirect_uri ) app$locals$tpapps <- rbind(app$locals$tpapps, rec) res$send_json(rec) }) app$get(authorize_endpoint, function(req, res) { # Missing or invalid client id client_id <- req$query$client_id if (is.null(client_id)) { res$ set_status(400L)$ send("Invalid authorization request, no client id") return() } else if (! client_id %in% app$locals$tpapps$client_id) { res$ set_status(400L)$ send("Invalid authorization request, unknown client id") return() } tpapps <- app$locals$tpapps tprec <- tpapps[match(client_id, tpapps$client_id), ] # Bad redirect URL? if (req$query$redirect_uri %||% "" != tprec$redirect_uri) { res$ set_status(400L)$ send(paste0( "Invalid authorization request, redirect URL mismatch: ", req$query$redirect_uri %||% "", " vs ", tprec$redirect_uri )) return() } state <- req$query$state %||% generate_token() app$locals$states <- c(app$locals$states, set_name(client_id, state)) html <- res$render("authorize", list(state = state, app = tprec$name)) res$ set_type("text/html")$ send(html) }) app$post(paste0(authorize_endpoint, "/decision"), function(req, res) { state <- req$form$state if (is.null(state) || ! state %in% names(app$locals$states)) { res$ set_status(400L)$ send("Invalid decision, no state") return() } client_id <- app$locals$states[state] tpapps <- app$locals$tpapps tprec <- tpapps[match(client_id, tpapps$client_id), ] app$local$states <- app$local$states[setdiff(names(app$local$states), state)] if (req$form$action %||% "" == "yes") { code <- generate_token() # TODO: make this app specific app$locals$codes <- c(app$locals$codes, code) red_uri <- paste0(tprec$redirect_uri, "?code=", code, "&state=", state) res$redirect(red_uri)$send() } else { res$ send("Maybe next time.") } }) local_app_seed <- function(.local_envir = parent.frame()) { old_seed <- get_seed() set_seed(app$locals$seed) defer({ app$locals$seed <- get_seed() set_seed(old_seed) }, envir = .local_envir) } new_access_token <- function(client_id, duration) { local_app_seed() token <- paste0("token-", generate_token()) new <- data.frame( stringsAsFactors = FALSE, client_id = client_id, token = token, expiry = Sys.time() + duration ) app$locals$tokens <- rbind(app$locals$tokens, new) token } new_refresh_token <- function(client_id, duration) { local_app_seed() token <- paste0("refresh-token-", generate_token()) new <- data.frame( stringsAsFactors = FALSE, client_id = client_id, token = token, expiry = Sys.time() + duration ) app$locals$refresh_tokens <- rbind(app$locals$refresh_tokens, new) token } expire_tokens <- function() { if (!is.null(app$locals$tokens)) { app$locals$tokens <- app$locals$tokens[ app$locals$tokens$expiry > Sys.time(),, drop = FALSE ] } if (!is.null(app$locals$refresh_tokens)) { app$locals$refresh_tokens <- app$locals$refresh_tokens[ app$locals$refresh_tokens$expiry > Sys.time(),, drop = FALSE ] } } is_valid_token <- function(token) { expire_tokens() token %in% app$locals$tokens$token } is_valid_refresh_token <- function(client_id, token) { expire_tokens() wh <- match(token, app$locals$refresh_tokens$token) if (is.na(wh)) return(FALSE) # client ID must match as well app$locals$refresh_tokens$client_id == client_id } # For refresh tokens app$post(token_endpoint, function(req, res) { if (req$form$grant_type != "refresh_token") return("next") client_id <- req$form$client_id tpapps <- app$locals$tpapps if (! client_id %in% tpapps$client_id) { res$ set_status(400L)$ send("Invalid client id") return() } tprec <- tpapps[match(client_id, tpapps$client_id), ] if (req$form$client_secret %||% "" != tprec$client_secret) { res$ set_status(400L)$ send("Invalid token request, client secret mismatch") return() } if (!is_valid_refresh_token(client_id, req$form$refresh_token)) { res$ set_status(400L)$ send_json(list(error = "invalid_request"), auto_unbox = TRUE) return() } res$send_json(list( access_token = new_access_token(client_id, access_duration), expiry = access_duration, refresh_token = new_refresh_token(client_id, refresh_duration) ), auto_unbox = TRUE) }) # For regular tokens app$post(token_endpoint, function(req, res) { if (req$form$grant_type %||% "" != "authorization_code") { res$ set_status(400L)$ send("Invalid grant type, must be 'authorization_code'") return() } if (! req$form$code %in% app$locals$codes) { res$ set_status(400L)$ send("Unknown authorization code") return() } tpapps <- app$locals$tpapps client_id <- req$form$client_id if (! client_id %in% tpapps$client_id) { res$ set_status(400L)$ send("Invalid client id") return() } tprec <- tpapps[match(client_id, tpapps$client_id), ] if (req$form$client_secret %||% "" != tprec$client_secret) { res$ set_status(400L)$ send("Invalid token request, client secret mismatch") return() } if (req$form$redirect_uri %||% "" != tprec$redirect_uri) { res$ set_status(400L)$ send("Invalid token request, redirect URL mismatch") return() } app$locals$codes <- setdiff(app$locals$codes, req$query$code) res$send_json(list( access_token = new_access_token(client_id, access_duration), expiry = access_duration, refresh_token = if (refresh) new_refresh_token(client_id, refresh_duration) ), auto_unbox = TRUE, pretty = TRUE) }) app$get("/locals", function(req, res) { res$ set_status(200L)$ send_json(list( apps = app$locals$tpapps, access = app$locals$tokens, refresh = app$locals$refresh_tokens ), auto_unbox = TRUE) }) app$is_authorized <- function(req, res) { expire_tokens() if (!("Authorization" %in% names(req$headers))) { res$ set_status(401L)$ send("Missing bearer token") return(FALSE) } token <- gsub("Bearer ", "", req$headers$Authorization[[1]]) if (!is_valid_token(token)) { res$ set_status(401L)$ send("Invalid bearer token") return(FALSE) } TRUE } app$get("/data", function(req, res) { if (!app$is_authorized(req, res)) return() res$send_json(list(data = "top secret!")) }) app } #' App representing the third-party app #' #' @includeRmd man/rmd-fragments/oauth2.Rmd description #' #' @details #' Endpoints: #' * `POST /login/config` Use this endpoint to configure the client ID #' and the client secret of the app, received from #' [oauth2_resource_app()] (or another resource app). You need to #' send in a JSON or URL encoded body: #' - `auth_url`, the authorization URL of the resource app. #' - `token_url`, the token URL of the resource app. #' - `client_id`, the client ID, received from the resource app. #' - `client_secret` the client secret, received from the resource #' app. #' * `GET /login` Use this endpoint to start the login process. It #' will redirect to the resource app for authorization and after the #' OAuth2.0 dance to `/login/redirect`. #' * `GET /login/redirect`, `POST /login/redirect` This is the #' redirect URI of the third party app. (Some HTTP clients redirect #' a `POST` to a `GET`, others don't, so it has both.) This endpoint #' is used by the resource app, and it received the `code` that can #' be exchanged to an access token and the `state` which was #' generated in `/login`. It contacts the resource app to get an #' access token, and then stores the token in its `app$locals` #' local variables. It fails with HTTP code 500 if it cannot obtain #' an access token. On success it returns a JSON dictionary with #' `access_token`, `expiry` and `refresh_token` (optionally) by #' default. This behavior can be changed by redefining the #' `app$redirect_hook()` function. #' * `GET /locals` returns the tokens that were obtained from the #' resource app. #' * `GET /data` is an endpoint that uses the obtained token(s) to #' connect to the `/data` endpoint of the resource app. The `/data` #' endpoint of the resource app needs authorization. It responds #' with the response of the resource app. It tries to refresh the #' access token of the app if needed. #' #' For more details see `vignette("oauth", package = "webfakes")`. #' #' @param name Name of the third-party app #' @return webfakes app #' @export #' @family OAuth2.0 functions oauth2_third_party_app <- function(name = "Third-Party app") { app <- new_app() app$use(mw_urlencoded()) app$use(mw_json()) app$locals$auth_url <- NA_character_ app$locals$token_url <- NA_character_ app$locals$client_id <- NA_character_ app$locals$client_secret <- NA_character_ app$post("/login/config", function(req, res) { if (is.null(req$json$auth_url) || is.null(req$json$token_url) || is.null(req$json$client_id) || is.null(req$json$client_secret)) { res$ set_status(400L)$ send("Need `client_id` and `client_secret` to config auth") return() } if (!is.na(app$locals$auth_url)) { res$ set_status(400L)$ send("Auth already configured") return() } app$locals$auth_url <- req$json$auth_url app$locals$token_url <- req$json$token_url app$locals$client_id <- req$json$client_id app$locals$client_secret <- req$json$client_secret res$send_json(list(response = "Authorization configured")) }) app$get("/login", function (req, res) { state <- generate_token() app$locals$state <- state url <- paste0( app$locals$auth_url, "?client_id=", app$locals$client_id, "&redirect_uri=", paste0(req$url, "/redirect"), "&state=", state ) res$redirect(url)$send() }) # I could not convince curl to redirect a POST to a GET, so both are good app$all("/login/redirect", function (req, res) { code <- req$query$code state <- req$query$state if (is.null(code) || is.null(state)) { res$ set_status(400L)$ send("Invalid request via auth server, no 'code' or 'state'.") return() } if (state != app$locals$state) { res$ set_status(400L)$ send("Unknown state in request via auth server") return() } # Get a token handle <- curl::new_handle() data <- charToRaw(paste0( "grant_type=authorization_code&", "code=", code, "&", "client_id=", app$locals$client_id, "&", "client_secret=", app$locals$client_secret, "&", "redirect_uri=", req$url )) curl::handle_setheaders( handle, "content-type" = "application/x-www-form-urlencoded" ) curl::handle_setopt( handle, customrequest = "POST", postfieldsize = length(data), postfields = data ) resp <- curl::curl_fetch_memory(app$locals$token_url, handle = handle) if (resp$status_code != 200L) { res$ set_status(500L)$ send(paste0( "Failed to acquire authorization token. ", rawToChar(resp$content) )) return() } tokens <- rawToChar(resp$content) app$locals$tokens <- jsonlite::fromJSON(tokens) app$redirect_hook(res, tokens) }) app$redirect_hook <- function(res, tokens) { res$ send_json(text = tokens) } app$get("/locals", function(req, res) { res$ set_status(200L)$ send_json(app$locals$tokens, auto_unbox = TRUE) }) get_data <- function() { auth <- paste("Bearer", app$locals$tokens$access_token) handle <- curl::new_handle() curl::handle_setheaders(handle, Authorization = auth) url <- modify_path(app$locals$token_url, "/data") curl::curl_fetch_memory(url, handle = handle) } try_refresh <- function() { refresh_token <- app$locals$tokens$refresh_token if (is.null(refresh_token)) return(FALSE) data <- charToRaw(paste0( "refresh_token=", refresh_token, "&", "grant_type=refresh_token" )) handle <- curl::new_handle() curl::handle_setheaders( handle, "content-type" = "application/x-www-form-urlencoded" ) curl::handle_setopt( handle, customrequest = "POST", postfieldsize = length(data), postfields = data ) resp <- curl::curl_fetch_memory(app$locals$token_url, handle = handle) if (resp$status_code != 200L) return(FALSE) tokens <- rawToChar(resp$content) app$locals$tokens <- jsonlite::fromJSON(tokens) TRUE } app$get("/data", function(req, res) { resp <- get_data() if (resp$status_code == 401) { if (try_refresh()) resp <- get_data() } res$ set_status(resp$status_code)$ set_type(resp$type)$ send(resp$content) }) } #' Helper function to log in to a third party OAuth2.0 app without a #' browser #' #' It works with [oauth2_resource_app()], and any third party app, #' including the fake [oauth2_third_party_app()]. #' #' See `test-oauth.R` in webfakes for an example. #' #' @param login_url The login URL of the third party app. #' @return A named list with #' * `login_response` The curl HTTP response object for the login #' page. #' * `token_response` The curl HTTP response object for submitting #' the login page. #' #' @family OAuth2.0 functions #' @export oauth2_login <- function(login_url) { login_resp <- curl::curl_fetch_memory(login_url) html <- rawToChar(login_resp$content) xml <- xml2::read_html(html) form <- xml2::xml_find_first(xml, "//form") input <- xml2::xml_find_first(form, "//input") actn <- xml2::xml_attr(form, "action") stnm <- xml2::xml_attr(input, "name") stvl <- xml2::xml_attr(input, "value") data <- charToRaw(paste0( stnm, "=", stvl, "&", "action=yes" )) handle2 <- curl::new_handle() curl::handle_setheaders( handle2, "content-type" = "application/x-www-form-urlencoded" ) curl::handle_setopt( handle2, customrequest = "POST", postfieldsize = length(data), postfields = data ) psurl <- parse_url(login_resp$url) actn_url <- paste0(psurl$protocol, "://", psurl$host, actn) token_resp <- curl::curl_fetch_memory(actn_url, handle = handle2) list( login_response = login_resp, token_response = token_resp ) } #' Helper function to use httr's OAuth2.0 functions #' non-interactively, e.g. in test cases #' #' To perform an automatic acknowledgement and log in for a #' local OAuth2.0 app, run by httr, wrap the expression that #' obtains the OAuth2.0 token in `oauth2_httr_login()`. #' #' In interactive sessions, `oauth2_httr_login()` overrides the #' `browser` option, and when httr opens a browser page, it #' calls [oauth2_login()] in a subprocess. #' #' In non-interactive sessions, httr does not open a browser page, #' only messages the user to do it manually. `oauth2_httr_login()` #' listens for these messages, and calls [oauth2_login()] in a #' subprocess. #' #' @param expr Expression that calls [httr::oauth2.0_token()], #' either directly, or indirectly. #' @return The return value of `expr`. #' #' @seealso See `?vignette("oauth", package = "webfakes")` for a case #' study that uses this function. #' #' @export #' @family OAuth2.0 functions oauth2_httr_login <- function(expr) { proc <- NULL if (interactive()) { local_options(browser = function(url) { proc <<- callr::r_bg( oauth2_login, list(url), package = "webfakes" ) }) expr } else { withCallingHandlers( expr, message = function(msg) { if (grepl("^Please point your browser to the following url:", msg$message)) { invokeRestart("muffleMessage") } if (grepl("^http", msg$message)) { proc <<- callr::r_bg( oauth2_login, list(trimws(msg$message)), package = "webfakes" ) invokeRestart("muffleMessage") } } ) } } webfakes/R/mw-cgi.R0000644000176200001440000001335614740430506013563 0ustar liggesusers #' Middleware that calls a CGI script #' #' You can use it as an unconditional middleware in `app$use()`, #' as a handler on `app$get()`, `app$post()`, etc., or you can call it #' from a handler. See examples below. #' #' @param command External command to run. #' @param args Arguments to pass to the external command. #' @param timeout Timeout for the external command. If the command does #' not terminate in time, the web server kills it and returns an 500 #' response. #' @return A function with signature #' ``` #' function(req, res, env = character()) #' ``` #' #' See [RFC 3875](https://datatracker.ietf.org/doc/html/rfc3875) #' for details on the CGI protocol. #' #' The request body (if any) is passed to the external command as standard #' intput. `mw_cgi()` sets `CONTENT_LENGTH`, `CONTENT_TYPE`, #' `GATEWAY_INTERFACE`, `PATH_INFO`, `QUERY_STRING`, `REMOTE_ADDR`, #' `REMOTE_HOST`, `REMOTE_USER`, `REQUEST_METHOD`, `SERVER_NAME`, #' `SERVER_PORT`, `SERVER_PROTOCOL`, `SERVER_SOFTEWARE`. #' #' It does not currently set the `AUTH_TYPE`, `PATH_TRANSLATED`, #' `REMOTE_IDENT`, `SCRIPT_NAME` environment variables. #' #' The standard output of the external command is used to set the #' response status code, the response headers and the response body. #' Example output from git's CGI: #' ``` #' Status: 200 OK #' Expires: Fri, 01 Jan 1980 00:00:00 GMT #' Pragma: no-cache #' Cache-Control: no-cache, max-age=0, must-revalidate #' Content-Type: application/x-git-upload-pack-advertisement #' #' 000eversion 2 #' 0015agent=git/2.42.0 #' 0013ls-refs=unborn #' 0020fetch=shallow wait-for-done #' 0012server-option #' 0017object-format=sha1 #' 0010object-info #' 0000 #' ``` #' #' @family middleware #' @export #' @examples #' app <- new_app() #' app$use(mw_cgi("echo", "Status: 200\n\nHello")) #' app #' #' app2 <- new_app() #' app2$get("/greet", mw_cgi("echo", "Status: 200\n\nHello")) #' app2 #' #' # Using `mw_cgi()` in a handler, you can pass extra environment variables #' app3 <- new_app() #' cgi <- mw_cgi("echo", "Status: 200\n\nHello") #' app2$get("/greet", function(req, res) { #' cgi(req, res, env = c("EXTRA_VAR" = "EXTRA_VALUE")) #' }) #' app3 mw_cgi <- function(command, args = character(), timeout = as.difftime(Inf, units = "secs")) { command args timeout <- if (timeout == Inf) { -1 } else { as.double(timeout, units = "secs") * 1000 } function(req, res, env = character()) { all_env <- c("current", cgi_env(req), env) inp <- tempfile() out <- tempfile() err <- tempfile() on.exit(unlink(c(inp, out, err)), add = TRUE) writeBin(req$.body %||% raw(), inp) px <- processx::process$new( command, args, env = all_env, stdin = inp, stdout = out, stderr = err ) px$wait(timeout) output <- parse_cgi_output(px, out, err) res$set_status(output$status) for (idx in seq_along(output$headers)) { res$set_header(names(output$headers)[idx], output$headers[[idx]]) } res$send(output$body) } } parse_cgi_output <- function(px, out, err) { if (px$is_alive() || px$get_exit_status() != 0) { px$kill() err <- tryCatch(read_char(err), error = function(e) "???") return(list( status = 500L, headers = c("content-type" = "text/plain"), body = paste0("Internal git error: ", err) )) } out <- read_bin(out) err <- read_char(err) cgi_res <- split_cgi_output(out) headers <- cgi_res$headers names(headers) <- tolower(names(headers)) status <- parse_status(headers[["status"]] %||% "200") headers <- headers[names(headers) != "status"] list(status = status, headers = headers, body = cgi_res$body) } cgi_env <- function(req) { url <- parse_url(req$url) c( CONTENT_LENGTH = length(req$.body), CONTENT_TYPE = if (!is.null(req$get_header)) req$get_header("content-type") %||% "", GATEWAY_INTERFACE = "CGI/1.1", PATH_INFO = req$path, QUERY_STRING = req$query_string, REMOTE_ADDR = req$remote_addr, REMOTE_HOST = req$remote_addr, REMOTE_USER = "anonymous", REQUEST_METHOD = toupper(req$method), SERVER_NAME = url$host, SERVER_PORT = url$port, SERVER_PROTOCOL = paste0("http/", req$http_version), SERVER_SOFTWARE = "https://github.com/r-lib/webfakes" ) } split_cgi_output <- function(x) { nlnl <- grepRaw("\r?\n\r?\n", x)[1] if (is.na(nlnl)) { stop("Invalid response from git cgi, no headers?") } headers <- parse_headers(rawToChar(x[1:(nlnl - 1L)])) body <- x[nlnl:length(x)] ndrop <- 1L while (body[ndrop] != 0x0a) ndrop <- ndrop + 1L ndrop <- ndrop + 1L while (body[ndrop] != 0x0a) ndrop <- ndrop + 1L body <- utils::tail(body, -ndrop) list(headers = headers, body = body) } parse_status <- function(x) { status <- as.integer(strsplit(x, " ", fixed = TRUE)[[1]][1]) if (is.na(status)) { stop("Invalid status from git cgi: ", x) } status } parse_headers <- function (txt) { headers <- grep(":", parse_headers0(txt), fixed = TRUE, value = TRUE) out <- lapply(headers, split_header) names <- tolower(vapply(out, `[[`, character(1), 1)) values <- lapply(lapply(out, `[[`, 2), trimws) names(values) <- names values } parse_headers0 <- function (txt, multiple = FALSE) { if (!length(txt)) return(NULL) if (is.raw(txt)) { txt <- rawToChar(txt) } stopifnot(is.character(txt)) if (length(txt) > 1) { txt <- paste(txt, collapse = "\n") } sets <- strsplit(txt, "\\r\\n\\r\\n|\\n\\n|\\r\\r")[[1]] headers <- strsplit(sets, "\\r\\n|\\n|\\r") if (multiple) { headers } else { headers[[length(headers)]] } } split_header <- function(x) { pos <- grepRaw(":", x, fixed = TRUE)[1] if (is.na(pos)) { stop("Invalid response header from git cgi: ", x) } c(substr(x, 1, pos - 1L), substr(x, pos + 1L, nchar(x))) } webfakes/R/mw-urlencoded.R0000644000176200001440000000132314172041777015143 0ustar liggesusers #' Middleware to parse an url-encoded request body #' #' This is typically data from a form. The parsed data is added #' as the `form` element of the request object. #' #' @param type Content type to match before parsing. If it does not #' match, then the request object is not modified. #' @return Handler function. #' #' @family middleware #' @export #' @examples #' app <- new_app() #' app$use(mw_urlencoded()) #' app mw_urlencoded <- function(type = "application/x-www-form-urlencoded") { function(req, res) { ct <- req$get_header("Content-Type") %||% "" if (! ct %in% tolower(type)) return("next") if (!is.null(req$.body)) { req$form <- parse_query(rawToChar(req$.body)) } "next" } } webfakes/R/request.R0000644000176200001440000000531314740243712014063 0ustar liggesusers #' A webfakes request object #' #' webfakes creates a `webfakes_request` object for every incoming HTTP #' request. This object is passed to every matched route and middleware, #' until the response is sent. It has reference semantics, so handlers #' can modify it. #' #' Fields and methods: #' #' * `app`: The `webfakes_app` object itself. #' * `headers`: Named list of HTTP request headers. #' * `hostname`: The Host header, the server hostname and maybe port. #' * `method`: HTTP method. #' * `path`: Server path. #' * `protocol`: `"http"` or `"https"`. #' * `query_string`: The raw query string, without the starting `?`. #' * `query`: Parsed query parameters in a named list. #' * `remote_addr`: String, the domain name or IP address of the client. #' webfakes runs on the localhost, so this is `127.0.0.1`. #' * `url`: The full URL of the request. #' * `get_header(field)`: Function to query a request header. Returns #' `NULL` if the header is not present. #' #' Body parsing middleware adds additional fields to the request object. #' See [mw_raw()], [mw_text()], [mw_json()], [mw_multipart()] and #' [mw_urlencoded()]. #' #' @seealso [webfakes_response] for the webfakes response object. #' @name webfakes_request #' @examples #' # This is how you can see the request and response objects: #' app <- new_app() #' app$get("/", function(req, res) { #' browser() #' res$send("done") #' }) #' app #' #' # Now start this app on a port: #' # app$listen(3000) #' # and connect to it from a web browser: http://127.0.0.1:3000 #' # You can also use another R session to connect: #' # httr::GET("http://127.0.0.1:3000") #' # or the command line curl tool: #' # curl -v http://127.0.0.1:3000 #' # The app will stop while processing the request. NULL new_request <- function(app, self) { if (isTRUE(self$.has_methods)) return(self) parsed_headers <- self$headers self$.has_methods <- TRUE self$app <- app self$headers <- parsed_headers self$hostname <- parsed_headers$host self$method <- tolower(self$method) self$protocol <- "http" self$query = parse_query(self$query_string) self$get_header <- function(field) { h <- self$headers names(h) <- tolower(names(h)) h[[tolower(field)]] } rm(parsed_headers) self$res <- new_response(app, self) class(self) <- c("webfakes_request", class(self)) self } parse_query <- function(query) { query <- sub("^[?]", "", query) query <- chartr("+", " ", query) argstr <- strsplit(query, "&", fixed = TRUE)[[1]] argstr <- strsplit(argstr, "=", fixed = TRUE) keys <- vapply(argstr, function(x) utils::URLdecode(x[[1]]), character(1)) vals <- lapply(argstr, function(x) { if (length(x) == 2) utils::URLdecode(x[[2]]) else "" }) structure(vals, names = keys) } webfakes/R/mime.R0000644000176200001440000002500714172041777013332 0ustar liggesusers mime_find <- function(ext) { stopifnot(is_string(ext)) m <- mime_types[ext] if (is.na(m)) { ew <- str_is_suffix(ext, names(mime_types_sfx)) m <- mime_types_sfx[ew] } c(m, NA_character_)[1] } mime_types_sfx <- c( `3gpp` = "audio/3gpp", `jpm` = "video/jpm", `mp3` = "audio/mp3", `rtf` = "text/rtf", `wav` = "audio/wave", `x3db` = "model/x3d+binary", `x3dv` = "model/x3d+vrml", `xml` = "text/xml" ) mime_types <- c( `3g2` = "video/3gpp2", `3gp` = "video/3gpp", `3gpp` = "video/3gpp", `3mf` = "model/3mf", ac = "application/pkix-attr-cert", adp = "audio/adpcm", ai = "application/postscript", apng = "image/apng", appcache = "text/cache-manifest", asc = "application/pgp-signature", atom = "application/atom+xml", atomcat = "application/atomcat+xml", atomsvc = "application/atomsvc+xml", au = "audio/basic", aw = "application/applixware", bdoc = "application/bdoc", bin = "application/octet-stream", bmp = "image/bmp", bpk = "application/octet-stream", buffer = "application/octet-stream", ccxml = "application/ccxml+xml", cdmia = "application/cdmi-capability", cdmic = "application/cdmi-container", cdmid = "application/cdmi-domain", cdmio = "application/cdmi-object", cdmiq = "application/cdmi-queue", cer = "application/pkix-cert", cgm = "image/cgm", class = "application/java-vm", coffee = "text/coffeescript", conf = "text/plain", cpt = "application/mac-compactpro", crl = "application/pkix-crl", css = "text/css", csv = "text/csv", cu = "application/cu-seeme", davmount = "application/davmount+xml", dbk = "application/docbook+xml", deb = "application/octet-stream", def = "text/plain", deploy = "application/octet-stream", `disposition-notification` = "message/disposition-notification", dist = "application/octet-stream", distz = "application/octet-stream", dll = "application/octet-stream", dmg = "application/octet-stream", dms = "application/octet-stream", doc = "application/msword", dot = "application/msword", drle = "image/dicom-rle", dssc = "application/dssc+der", dtd = "application/xml-dtd", dump = "application/octet-stream", ear = "application/java-archive", ecma = "application/ecmascript", elc = "application/octet-stream", emf = "image/emf", eml = "message/rfc822", emma = "application/emma+xml", eps = "application/postscript", epub = "application/epub+zip", es = "application/ecmascript", exe = "application/octet-stream", exi = "application/exi", exr = "image/aces", ez = "application/andrew-inset", fits = "image/fits", g3 = "image/g3fax", gbr = "application/rpki-ghostbusters", geojson = "application/geo+json", gif = "image/gif", glb = "model/gltf-binary", gltf = "model/gltf+json", gml = "application/gml+xml", gpx = "application/gpx+xml", gram = "application/srgs", grxml = "application/srgs+xml", gxf = "application/gxf", gz = "application/gzip", h261 = "video/h261", h263 = "video/h263", h264 = "video/h264", heic = "image/heic", heics = "image/heic-sequence", heif = "image/heif", heifs = "image/heif-sequence", hjson = "application/hjson", hlp = "application/winhlp", hqx = "application/mac-binhex40", htm = "text/html", html = "text/html", ics = "text/calendar", ief = "image/ief", ifb = "text/calendar", iges = "model/iges", igs = "model/iges", img = "application/octet-stream", `in` = "text/plain", ini = "text/plain", ink = "application/inkml+xml", inkml = "application/inkml+xml", ipfix = "application/ipfix", iso = "application/octet-stream", jade = "text/jade", jar = "application/java-archive", jls = "image/jls", jp2 = "image/jp2", jpe = "image/jpeg", jpeg = "image/jpeg", jpf = "image/jpx", jpg = "image/jpeg", jpg2 = "image/jp2", jpgm = "video/jpm", jpgv = "video/jpeg", jpm = "image/jpm", jpx = "image/jpx", js = "application/javascript", json = "application/json", json5 = "application/json5", jsonld = "application/ld+json", jsonml = "application/jsonml+json", jsx = "text/jsx", jxr = "image/jxr", kar = "audio/midi", ktx = "image/ktx", less = "text/less", list = "text/plain", litcoffee = "text/coffeescript", log = "text/plain", lostxml = "application/lost+xml", lrf = "application/octet-stream", m1v = "video/mpeg", m21 = "application/mp21", m2a = "audio/mpeg", m2v = "video/mpeg", m3a = "audio/mpeg", m4a = "audio/mp4", m4p = "application/mp4", ma = "application/mathematica", mads = "application/mads+xml", man = "text/troff", manifest = "text/cache-manifest", map = "application/json", mar = "application/octet-stream", markdown = "text/markdown", mathml = "application/mathml+xml", mb = "application/mathematica", mbox = "application/mbox", md = "text/markdown", mdx = "text/mdx", me = "text/troff", mesh = "model/mesh", meta4 = "application/metalink4+xml", metalink = "application/metalink+xml", mets = "application/mets+xml", mft = "application/rpki-manifest", mid = "audio/midi", midi = "audio/midi", mime = "message/rfc822", mj2 = "video/mj2", mjp2 = "video/mj2", mjs = "application/javascript", mml = "text/mathml", mods = "application/mods+xml", mov = "video/quicktime", mp2 = "audio/mpeg", mp21 = "application/mp21", mp2a = "audio/mpeg", mp3 = "audio/mpeg", mp4 = "video/mp4", mp4a = "audio/mp4", mp4s = "application/mp4", mp4v = "video/mp4", mpd = "application/dash+xml", mpe = "video/mpeg", mpeg = "video/mpeg", mpg = "video/mpeg", mpg4 = "video/mp4", mpga = "audio/mpeg", mrc = "application/marc", mrcx = "application/marcxml+xml", ms = "text/troff", mscml = "application/mediaservercontrol+xml", msh = "model/mesh", msi = "application/octet-stream", msm = "application/octet-stream", msp = "application/octet-stream", mxf = "application/mxf", mxml = "application/xv+xml", n3 = "text/n3", nb = "application/mathematica", nq = "application/n-quads", nt = "application/n-triples", oda = "application/oda", oga = "audio/ogg", ogg = "audio/ogg", ogv = "video/ogg", ogx = "application/ogg", omdoc = "application/omdoc+xml", onepkg = "application/onenote", onetmp = "application/onenote", onetoc = "application/onenote", onetoc2 = "application/onenote", opf = "application/oebps-package+xml", otf = "font/otf", owl = "application/rdf+xml", oxps = "application/oxps", p10 = "application/pkcs10", p7c = "application/pkcs7-mime", p7m = "application/pkcs7-mime", p7s = "application/pkcs7-signature", p8 = "application/pkcs8", pdf = "application/pdf", pfr = "application/font-tdpfr", pgp = "application/pgp-encrypted", pkg = "application/octet-stream", pki = "application/pkixcmp", pkipath = "application/pkix-pkipath", pls = "application/pls+xml", png = "image/png", prf = "application/pics-rules", ps = "application/postscript", pskcxml = "application/pskc+xml", qt = "video/quicktime", raml = "application/raml+yaml", rdf = "application/rdf+xml", rif = "application/reginfo+xml", rl = "application/resource-lists+xml", rld = "application/resource-lists-diff+xml", rmi = "audio/midi", rnc = "application/relax-ng-compact-syntax", rng = "application/xml", roa = "application/rpki-roa", roff = "text/troff", rq = "application/sparql-query", rs = "application/rls-services+xml", rsd = "application/rsd+xml", rss = "application/rss+xml", rtf = "application/rtf", rtx = "text/richtext", s3m = "audio/s3m", sbml = "application/sbml+xml", scq = "application/scvp-cv-request", scs = "application/scvp-cv-response", sdp = "application/sdp", ser = "application/java-serialized-object", setpay = "application/set-payment-initiation", setreg = "application/set-registration-initiation", sgi = "image/sgi", sgm = "text/sgml", sgml = "text/sgml", shex = "text/shex", shf = "application/shf+xml", shtml = "text/html", sieve = "application/sieve", sig = "application/pgp-signature", sil = "audio/silk", silo = "model/mesh", siv = "application/sieve", slim = "text/slim", slm = "text/slim", smi = "application/smil+xml", smil = "application/smil+xml", snd = "audio/basic", so = "application/octet-stream", spp = "application/scvp-vp-response", spq = "application/scvp-vp-request", spx = "audio/ogg", sru = "application/sru+xml", srx = "application/sparql-results+xml", ssdl = "application/ssdl+xml", ssml = "application/ssml+xml", stk = "application/hyperstudio", stl = "model/stl", styl = "text/stylus", stylus = "text/stylus", svg = "image/svg+xml", svgz = "image/svg+xml", t = "text/troff", t38 = "image/t38", tei = "application/tei+xml", teicorpus = "application/tei+xml", text = "text/plain", tfi = "application/thraud+xml", tfx = "image/tiff-fx", tif = "image/tiff", tiff = "image/tiff", tr = "text/troff", ts = "video/mp2t", tsd = "application/timestamped-data", tsv = "text/tab-separated-values", ttc = "font/collection", ttf = "font/ttf", ttl = "text/turtle", txt = "text/plain", u8dsn = "message/global-delivery-status", u8hdr = "message/global-headers", u8mdn = "message/global-disposition-notification", u8msg = "message/global", uri = "text/uri-list", uris = "text/uri-list", urls = "text/uri-list", vcard = "text/vcard", vrml = "model/vrml", vtt = "text/vtt", vxml = "application/voicexml+xml", war = "application/java-archive", wasm = "application/wasm", wav = "audio/wav", weba = "audio/webm", webm = "video/webm", webmanifest = "application/manifest+json", webp = "image/webp", wgt = "application/widget", wmf = "image/wmf", woff = "font/woff", woff2 = "font/woff2", wrl = "model/vrml", wsdl = "application/wsdl+xml", wspolicy = "application/wspolicy+xml", x3d = "model/x3d+xml", x3db = "model/x3d+fastinfoset", x3dbz = "model/x3d+binary", x3dv = "model/x3d-vrml", x3dvz = "model/x3d+vrml", x3dz = "model/x3d+xml", xaml = "application/xaml+xml", xdf = "application/xcap-diff+xml", xdssc = "application/dssc+xml", xenc = "application/xenc+xml", xer = "application/patch-ops-error+xml", xht = "application/xhtml+xml", xhtml = "application/xhtml+xml", xhvml = "application/xv+xml", xm = "audio/xm", xml = "application/xml", xop = "application/xop+xml", xpl = "application/xproc+xml", xsd = "application/xml", xsl = "application/xml", xslt = "application/xslt+xml", xspf = "application/xspf+xml", xvm = "application/xv+xml", xvml = "application/xv+xml", yaml = "text/yaml", yang = "application/yang", yin = "application/yin+xml", yml = "text/yaml", zip = "application/zip" ) webfakes/R/git-app.R0000644000176200001440000000406614740243712013740 0ustar liggesusers#' Web app that acts as a git http server #' #' It is useful for tests that need an HTTP git server. #' #' @param git_root Path to the root of the directory tree to be served. #' @param git_cmd Command to call, by default it is `"git"`. It may also #' be a full path to git. #' @param git_timeout A `difftime` object, time limit for the git #' command. #' @param filter Whether to support the `filter` capability in the server. #' @param cleanup Whether to clean up `git_root` when the app is #' garbage collected. #' #' @export #' @examplesIf FALSE #' dir.create(tmp <- tempfile()) #' setwd(tmp) #' system("git clone --bare https://github.com/cran/crayon") #' system("git clone --bare https://github.com/cran/glue") #' app <- git_app(tmp) #' git <- new_app_process(app) #' system(paste("git ls-remote", git$url("/crayon"))) git_app <- function(git_root, git_cmd = "git", git_timeout = as.difftime(1, units = "mins"), filter = TRUE, cleanup = TRUE) { app <- webfakes::new_app() app$locals$git_root <- git_root app$locals$git_timeout <- as.double(git_timeout, units = "secs") * 1000 app$locals$git_config <- tempfile() reg.finalizer(app, function(app0) unlink(app$locals$git_config), TRUE) writeLines( c( "[uploadpack]", paste0("\tallowFilter = ", if (isTRUE(filter)) "true" else "false") ), app$locals$git_config ) if (cleanup) { reg.finalizer( app, function(app) unlink(app$locals$git_root, recursive = TRUE), TRUE ) } cgi <- mw_cgi(git_cmd, "http-backend", timeout = git_timeout) handler <- function(req, res) { env <- c( GIT_CONFIG_GLOBAL = req$app$locals$git_config, GIT_HTTP_EXPORT_ALL = "true", GIT_PROJECT_ROOT = req$app$locals$git_root, GIT_PROTOCOL = req$get_header("Git-Protocol") %||% "", HTTP_GIT_PROTOCOL = req$get_header("Git-Protocol") %||% "" ) cgi(req, res, env = env) } re_all <- new_regexp("^(?.*)$") app$get(re_all, handler) app$post(re_all, handler) app } webfakes/R/docs.R0000644000176200001440000000021014172041777013320 0ustar liggesusers #' @title webfakes glossary #' @name glossary #' @section Webfakes glossary: #' #' ```{r child = "vignettes/glossary.Rmd"} #' ``` NULL webfakes/R/app.R0000644000176200001440000004373314740243712013163 0ustar liggesusers pkg_data <- new.env(parent = emptyenv()) #' Create a new web application #' #' @details #' The typical workflow of creating a web application is: #' #' 1. Create a `webfakes_app` object with `new_app()`. #' 1. Add middleware and/or routes to it. #' 1. Start is with the `webfakes_app$listen()` method, or start it in #' another process with [new_app_process()]. #' 1. Make queries to the web app. #' 1. Stop it via `CTRL+C` / `ESC`, or, if it is running in another #' process, with the `$stop()` method of [new_app_process()]. #' #' A web application can be #' * restarted, #' * saved to disk, #' * copied to another process using the callr package, or a similar way, #' * embedded into a package, #' * extended by simply adding new routes and/or middleware. #' #' The webfakes API is very much influenced by the #' [express.js](http://expressjs.com/) project. #' #' ## Create web app objects #' #' ```r #' new_app() #' ``` #' #' `new_app()` returns a `webfakes_app` object the has the methods listed #' on this page. #' #' An app is an environment with S3 class `webfakes_app`. #' #' ## The handler stack #' #' An app has a stack of handlers. Each handler can be a route or #' middleware. The differences between the two are: #' * A route is bound to one or more paths on the web server. Middleware #' is not (currently) bound to paths, but run for all paths. #' * A route is usually (but not always) the end of the handler stack for #' a request. I.e. a route takes care of sending out the response to #' the request. Middleware typically performs some action on the request #' or the response, and then the next handler in the stack is invoked. #' #' ## Routes #' #' The following methods define routes. Each method corresponds to the #' HTTP verb with the same name, except for `app$all()`, which creates a #' route for all HTTP methods. #' #' ```r #' app$all(path, ...) #' app$delete(path, ...) #' app$get(path, ...) #' app$head(path, ...) #' app$patch(path, ...) #' app$post(path, ...) #' app$put(path, ...) #' ... (see list below) #' ``` #' #' * `path` is a path specification, see 'Path specification' below. #' * `...` is one or more handler functions. These will be placed in the #' handler stack, and called if they match an incoming HTTP request. #' See 'Handler functions' below. #' #' webfakes also has methods for the less frequently used HTTP verbs: #' `CONNECT`, `MKCOL`, `OPTIONS`, `PROPFIND`, `REPORT`. (The method #' names are always in lowercase.) #' #' If a request is not handled by any routes (or handler functions in #' general), then webfakes will send a simple HTTP 404 response. #' #' ## Middleware #' #' `app$use()` adds a middleware to the handler stack. A middleware is #' a handler function, see 'Handler functions' below. webfakes comes with #' middleware to perform common tasks: #' #' * [mw_cookie_parser()] parses `Cookie` headers. #' * [mw_etag()] adds an `ETag` header to the response. #' * [mw_json()] parses JSON request bodies. #' * [mw_log()] logs each requests to standard output, or another connection. #' * [mw_multipart()] parses multipart request bodies. #' * [mw_range_parser()] parses `Range` headers. #' * [mw_raw()] parses raw request bodies. #' * [mw_static()] serves static files from a directory. #' * [mw_text()] parses plain text request bodies. #' * [mw_urlencoded()] parses URL encoded request bodies. #' #' ```r #' app$use(..., .first = FALSE) #' ``` #' #' * `...` is a set of (middleware) handler functions. They are added to #' the handler stack, and called for every HTTP request. (Unless an HTTP #' response is created before reaching this point in the handler stack.) #' * `.first` set to `TRUE` is you want to add the handler function #' to the bottom of the stack. #' #' ## Handler functions #' #' A handler function is a route or middleware. A handler function is #' called by webfakes with the incoming HTTP request and the outgoing #' HTTP response objects (being built) as arguments. The handler function #' may query and modify the members of the request and/or the response #' object. If it returns the string `"next"`, then it is _not_ a terminal #' handler, and once it returns, webfakes will move on to call the next #' handler in the stack. #' #' A typical route: #' #' ```r #' app$get("/user/:id", function(req, res) { #' id <- req$params$id #' ... #' res$ #' set_status(200L)$ #' set_header("X-Custom-Header", "foobar")$ #' send_json(response, auto_unbox = TRUE) #' }) #' ``` #' #' * The handler belongs to an API path, which is a wildcard path in #' this case. It matches `/user/alice`, `/user/bob`, etc. The handler #' will be only called for GET methods and matching API paths. #' * The handler receives the request (`req`) and the response (`res`). #' * It sets the HTTP status, additional headers, and sends the data. #' (In this case the `webfakes_response$send_json()` method automatically #' converts `response` to JSON and sets the `Content-Type` and #' `Content-Length` headers. #' * This is a terminal handler, because it does _not_ return `"next"`. #' Once this handler function returns, webfakes will send out the HTTP #' response. #' #' A typical middleware: #' #' ```r #' app$use(function(req, res) { #' ... #' "next" #' }) #' ```` #' #' * There is no HTTP method and API path here, webfakes will call the #' handler for each HTTP request. #' * This is not a terminal handler, it does return `"next"`, so after it #' returns webfakes will look for the next handler in the stack. #' #' ## Errors #' #' If a handler function throws an error, then the web server will return #' a HTTP 500 `text/plain` response, with the error message as the #' response body. #' #' ## Request and response objects #' #' See [webfakes_request] and [webfakes_response] for the methods of the #' request and response objects. #' #' ## Path specification #' #' Routes are associated with one or more API paths. A path specification #' can be #' #' * A "plain" (i.e. without parameters) string. (E.g. `"/list"`.) #' * A parameterized string. (E.g. `"/user/:id"`.) #' * A regular expression created via [new_regexp()] function. #' * A list or character vector of the previous ones. (Regular expressions #' must be in a list.) #' #' ## Path parameters #' #' Paths that are specified as parameterized strings or regular expressions #' can have parameters. #' #' For parameterized strings the keys may contain letters, numbers and #' underscores. When webfakes matches an API path to a handler with a #' parameterized string path, the parameters will be added to the #' request, as `params`. I.e. in the handler function (and subsequent #' handler functions, if the current one is not terminal), they are #' available in the `req$params` list. #' #' For regular expressions, capture groups are also added as parameters. #' It is best to use named capture groups, so that the parameters are in #' a named list. #' #' If the path of the handler is a list of parameterized strings or #' regular expressions, the parameters are set according to the first #' matching one. #' #' ## Templates #' #' webfakes supports templates, using any template engine. It comes with #' a template engine that uses the glue package, see [tmpl_glue()]. #' #' `app$engine()` registers a template engine, for a certain file #' extension. The `$render()` method of [webfakes_response] #' can be called from the handler function to evaluate a template from a #' file. #' #' ```r #' app$engine(ext, engine) #' ``` #' #' * `ext`: the file extension for which the template engine is added. #' It should not contain the dot. E.g. `"html"', `"brew"`. #' * `engine`: the template engine, a function that takes the file path #' (`path`) of the template, and a list of local variables (`locals`) #' that can be used in the template. It should return the result. #' #' An example template engine that uses glue might look like this: #' #' ```r #' app$engine("txt", function(path, locals) { #' txt <- readChar(path, nchars = file.size(path)) #' glue::glue_data(locals, txt) #' }) #' ``` #' #' (The built-in [tmpl_glue()] engine has more features.) #' #' This template engine can be used in a handler: #' #' ```r #' app$get("/view", function(req, res) { #' txt <- res$render("test") #' res$ #' set_type("text/plain")$ #' send(txt) #' }) #' ``` #' #' The location of the templates can be set using the `views` configuration #' parameter, see the `$set_config()` method below. #' #' In the template, the variables passed in as `locals`, and also the #' response local variables (see `locals` in [webfakes_response]), are #' available. #' #' ## Starting and stopping #' #' ```r #' app$listen(port = NULL, opts = server_opts(), cleanup = TRUE) #' ``` #' #' * `port`: port to listen on. When `NULL`, the operating system will #' automatically select a free port. #' #' * `opts`: options to the web server. See [server_opts()] for the #' list of options and their default values. #' #' * `cleanup`: stop the server (with an error) if the standard input #' of the process is closed. This is handy when the app runs in a #' `callr::r_session` subprocess, because it stops the app (and the #' subprocess) if the main process has terminated. #' #' This method does not return, and can be interrupted with `CTRL+C` / `ESC` #' or a SIGINT signal. See [new_app_process()] for interrupting an app that #' is running in another process. #' #' When `port` is `NULL`, the operating system chooses a port where the #' app will listen. To be able to get the port number programmatically, #' before the listen method blocks, it advertises the selected port in a #' `webfakes_port` condition, so one can catch it: #' #' webfakes by default binds only to the loopback interface at 127.0.0.1, so #' the webfakes web app is never reachable from the network. #' #' ```r #' withCallingHandlers( #' app$listen(), #' "webfakes_port" = function(msg) print(msg$port) #' ) #' ``` #' #' ## Logging #' #' webfakes can write an access log that contains an entry for all incoming #' requests, and also an error log for the errors that happen while #' the server is running. This is the default behavior for local app #' (the ones started by `app$listen()` and for remote apps (the ones #' started via `new_app_process()`: #' #' * Local apps do not write an access log by default. #' * Remote apps write an access log into the #' `/webfakes//access.log` file, where `` is the #' session temporary directory of the _main process_, and `` is #' the process id of the _subprocess_. #' * Local apps write an error log to `/webfakes/error.log`, where #' `` is the session temporary directory of the current process. #' * Remote app write an error log to the `/webfakes//error.log`, #' where `` is the session temporary directory of the #' _main process_ and `` is the process id of the _subprocess_`. #' #' See [server_opts()] for changing the default logging behavior. #' #' ## Shared app data #' #' ```r #' app$locals #' ``` #' #' It is often useful to share data between handlers and requests in an #' app. `app$locals` is an environment that supports this. E.g. a #' middleware that counts the number of requests can be implemented as: #' #' ``` #' app$use(function(req, res) { #' locals <- req$app$locals #' if (is.null(locals$num)) locals$num <- 0L #' locals$num <- locals$num + 1L #' "next" #' }) #' ``` #' #' [webfakes_response] objects also have a `locals` environment, that is #' initially populated as a copy of `app$locals`. #' #' ## Configuration #' #' ```r #' app$get_config(key) #' app$set_config(key, value) #' ``` #' #' * `key`: configuration key. #' * `value`: configuration value. #' #' Currently used configuration values: #' #' * `views`: path where webfakes searches for templates. #' #' @return A new `webfakes_app`. #' @aliases webfakes_app #' @seealso [webfakes_request] for request objects, [webfakes_response] for #' response objects. #' @export #' @examples #' # see example web apps in the `/examples` directory in #' system.file(package = "webfakes", "examples") #' #' app <- new_app() #' app$use(mw_log()) #' #' app$get("/hello", function(req, res) { #' res$send("Hello there!") #' }) #' #' app$get(new_regexp("^/hi(/.*)?$"), function(req, res) { #' res$send("Hi indeed!") #' }) #' #' app$post("/hello", function(req, res) { #' res$send("Got it, thanks!") #' }) #' #' app #' #' # Start the app with: app$listen() #' # Or start it in another R session: new_app_process(app) new_app <- function() { self <- new_object( "webfakes_app", all = function(path, ...) { self$.stack <- c(self$.stack, parse_handlers("all", path, ...)) invisible(self) }, connect = function(path, ...) { self$.stack <- c(self$.stack, parse_handlers("connect", path, ...)) invisible(self) }, delete = function(path, ...) { self$.stack <- c(self$.stack, parse_handlers("delete", path, ...)) invisible(self) }, get = function(path, ...) { self$.stack <- c(self$.stack, parse_handlers("get", path, ...)) invisible(self) }, engine = function(ext, engine) { rec <- list(ext = ext, engine = engine) self$.engines <- c(self$.engines, list(rec)) invisible(self) }, get_config = function(key) { self$.config[[key]] }, head = function(path, ...) { self$.stack <- c(self$.stack, parse_handlers("head", path, ...)) invisible(self) }, listen = function(port = NULL, opts = server_opts(), cleanup = TRUE) { stopifnot(is.null(port) || is_port(port) || is_na_scalar(port)) if (is_na_scalar(port)) port <- NULL opts$port <- port self$.enable_keep_alive <- opts$enable_keep_alive opts$access_log_file <- sub("%p", Sys.getpid(), opts$access_log_file) opts$error_log_file <- sub("%p", Sys.getpid(), opts$error_log_file) self$.opts <- opts tryCatch(srv <- server_start(opts), error = function(err) { err$message <- paste(sep = "\n", err$message, self$.get_error_log()) stop(err) }) ports <- server_get_ports(srv) self$.port <- ports$port[1] message("Running webfakes web app on port ", self$.port) if (!is.na(opts$access_log_file)) { message("Access log file: ", opts$access_log_file) } if (!is.na(opts$error_log_file)) { message("Error log file: ", opts$error_log_file) } msg <- structure( list( port = self$.port, access_log = attr(srv, "options")$access_log_file, error_log = attr(srv, "options")$error_log_file ), class = c("webfakes_port", "callr_message", "condition") ) message(msg) on.exit(server_stop(srv), add = TRUE) while (TRUE) { req <- server_poll(srv, cleanup) tryCatch( self$.process_request(req), error = function(err) { cat(as.character(err), file = stderr()) response_send_error(req, as.character(err), 500L) } ) } }, mkcol = function(path, ...) { self$.stack <- c(self$.stack, parse_handlers("mkcol", path, ...)) invisible(self) }, options = function(path, ...) { self$.stack <- c(self$.stack, parse_handlers("options", path, ...)) invisible(self) }, patch = function(path, ...) { self$.stack <- c(self$.stack, parse_handlers("patch", path, ...)) invisible(self) }, post = function(path, ...) { self$.stack <- c(self$.stack, parse_handlers("post", path, ...)) invisible(self) }, propfind = function(path, ...) { self$.stack <- c(self$.stack, parse_handlers("propfind", path, ...)) invisible(self) }, put = function(path, ...) { self$.stack <- c(self$.stack, parse_handlers("put", path, ...)) invisible(self) }, report = function(path, ...) { self$.stack <- c(self$.stack, parse_handlers("report", path, ...)) invisible(self) }, set_config = function(key, value) { self$.config[[key]] <- value invisible(self) }, use = function(..., .first = FALSE) { mw <- parse_handlers("use", "*", ...) if (.first) { self$.stack <- c(mw, self$.stack) } else { self$.stack <- c(self$.stack, mw) } invisible(self) }, # Public data to be used in handlers locals = new.env(parent = parent.frame()), # Private data .port = NULL, .enable_keep_alive = NULL, .opts = NULL, # middleware stack .stack = list(), # view engines .engines = list(), # config .config = as.environment(list( views = file.path(getwd(), "views") )), # The request processing function .process_request = function(req) { req <- new_request(self, req) res <- req$res res$.delay <- NULL tryCatch({ for (i in sseq(res$.stackptr, length(self$.stack))) { handler <- self$.stack[[i]] m <- path_match(req$method, req$path, handler) if (!isFALSE(m)) { res$.i <- i if (is.list(m)) req$params <- m$params out <- handler$handler(req, res) if (!identical(out, "next")) break } } if (!res$.sent && is.null(res$.delay)) { if (!res$headers_sent) { res$send_status(404) } else if ((res$get_header("Transfer-Encoding") %||% "") == "chunked") { res$send_chunk(raw(0)) res$headers_sent <- TRUE res$send("") } else { res$send("") } } }, webfakes_error = function(err) { }) }, .get_error_log = function() { if (!is.na(self$.opts$error_log_file)) { paste0("Error log:\n", read_char(self$.opts$error_log_file)) } } ) self } parse_handlers <- function(method, path, ...) { handlers <- list(...) ans <- list() for (h in seq_along(handlers)) { handler <- handlers[[h]] if (is.function(handler)) { rec <- list( method = method, path = path, handler = handler, name = names(handlers)[h] ) ans <- c(ans, list(rec)) } else { stop("Invalid webfakes handler") } } ans } webfakes/src/0000755000176200001440000000000014740433474012642 5ustar liggesuserswebfakes/src/errors.h0000644000176200001440000000342414172041777014332 0ustar liggesusers #ifndef R_THROW_ERROR_H #define R_THROW_ERROR_H #ifndef _GNU_SOURCE #define _GNU_SOURCE 1 #endif #ifdef _WIN32 #include #else #include #endif #include #define R_THROW_ERROR(...) \ r_throw_error(__func__, __FILE__, __LINE__, __VA_ARGS__) SEXP r_throw_error(const char *func, const char *filename, int line, const char *msg, ...); #ifdef _WIN32 #define R_THROW_SYSTEM_ERROR(...) \ r_throw_system_error(__func__, __FILE__, __LINE__, (-1), NULL, __VA_ARGS__) #define R_THROW_SYSTEM_ERROR_CODE(errorcode, ...) \ r_throw_system_error(__func__, __FILE__, __LINE__, (errorcode), NULL, __VA_ARGS__) SEXP r_throw_system_error(const char *func, const char *filename, int line, DWORD errorcode, const char *sysmsg, const char *msg, ...); SEXP r_throw_posix_error(const char *func, const char *filename, int line, int errorcode, const char *sysmsg, const char *msg, ...); #define R_THROW_POSIX_ERROR(...) \ r_throw_posix_error(__func__, __FILE__, __LINE__, errno, NULL, __VA_ARGS__) #define R_THROW_POSIX_ERROR_CODE(errorcode, ...) \ r_throw_posix_error(__func__, __FILE__, __LINE__, errorcode, NULL, __VA_ARGS__) #else #define R_THROW_SYSTEM_ERROR(...) \ r_throw_system_error(__func__, __FILE__, __LINE__, errno, NULL, __VA_ARGS__) #define R_THROW_SYSTEM_ERROR_CODE(errorcode, ...) \ r_throw_system_error(__func__, __FILE__, __LINE__, errorcode, NULL, __VA_ARGS__) SEXP r_throw_system_error(const char *func, const char *filename, int line, int errorcode, const char *sysmsg, const char *msg, ...); #endif #endif webfakes/src/rweb.c0000644000176200001440000007036614740243712013753 0ustar liggesusers #include #include #include #include "civetweb.h" #include "errors.h" #include "cleancall.h" #include #include #ifdef _WIN32 #include #else #include #include #endif /* --------------------------------------------------------------------- */ /* registration */ /* --------------------------------------------------------------------- */ SEXP server_start(SEXP options); SEXP server_poll(SEXP rsrv, SEXP clean); SEXP server_stop(SEXP rsrv); SEXP server_get_ports(SEXP rsrv); SEXP response_delay(SEXP req, SEXP secs); SEXP response_send_headers(SEXP req); SEXP response_send(SEXP req); SEXP response_write(SEXP req, SEXP data); SEXP response_send_error(SEXP req, SEXP message, SEXP status); SEXP response_send_chunk(SEXP req, SEXP data); SEXP webfakes_crc32(SEXP v); static const R_CallMethodDef callMethods[] = { CLEANCALL_METHOD_RECORD, /* server */ { "server_start", (DL_FUNC) &server_start, 1 }, { "server_poll", (DL_FUNC) &server_poll, 2 }, { "server_stop", (DL_FUNC) &server_stop, 1 }, { "server_get_ports", (DL_FUNC) &server_get_ports, 1 }, /* request/response/connection */ { "response_delay", (DL_FUNC) &response_delay, 2 }, { "response_send_headers", (DL_FUNC) &response_send_headers, 1 }, { "response_send", (DL_FUNC) &response_send, 1 }, { "response_write", (DL_FUNC) &response_write, 2 }, { "response_send_error", (DL_FUNC) &response_send_error, 3 }, { "response_send_chunk", (DL_FUNC) &response_send_chunk, 2 }, /* others */ { "webfakes_crc32", (DL_FUNC) &webfakes_crc32, 1 }, { NULL, NULL, 0 } }; void R_init_webfakes(DllInfo *dll) { R_registerRoutines(dll, NULL, callMethods, NULL, NULL); R_useDynamicSymbols(dll, FALSE); R_forceSymbols(dll, TRUE); cleancall_fns_dot_call = Rf_findVar(Rf_install(".Call"), R_BaseEnv); /* Once we require some features we need to check the return value. */ mg_init_library(0); } /* --------------------------------------------------------------------- */ /* for older macOS versions */ /* --------------------------------------------------------------------- */ #if defined(__MACH__) && defined(__APPLE__) #include #include #include #include /* It doesn't really matter what these are defined to, as long as they are defined */ #ifndef CLOCK_REALTIME #define CLOCK_REALTIME 0 #endif #ifndef CLOCK_MONOTONIC #define CLOCK_MONOTONIC 1 #endif static int webfakes_clock_gettime(int clk_id, struct timespec *t) { memset(t, 0, sizeof(*t)); if (clk_id == CLOCK_REALTIME) { struct timeval now; int rv = gettimeofday(&now, NULL); if (rv) { return rv; } t->tv_sec = now.tv_sec; t->tv_nsec = now.tv_usec * 1000; return 0; } else if (clk_id == CLOCK_MONOTONIC) { static uint64_t clock_start_time = 0; static mach_timebase_info_data_t timebase_ifo = {0, 0}; uint64_t now = mach_absolute_time(); if (clock_start_time == 0) { kern_return_t mach_status = mach_timebase_info(&timebase_ifo); /* appease "unused variable" warning for release builds */ (void)mach_status; clock_start_time = now; } now = (uint64_t)((double)(now - clock_start_time) * (double)timebase_ifo.numer / (double)timebase_ifo.denom); t->tv_sec = now / 1000000000; t->tv_nsec = now % 1000000000; return 0; } return -1; /* EINVAL - Clock ID is unknown */ } #else #define webfakes_clock_gettime(a,b) clock_gettime(a,b) #endif /* --------------------------------------------------------------------- */ /* internals */ /* --------------------------------------------------------------------- */ #ifdef _WIN32 int check_stdin(void) { HANDLE hnd = GetStdHandle(STD_INPUT_HANDLE); if (hnd == INVALID_HANDLE_VALUE) { R_THROW_SYSTEM_ERROR("Cannot get stdin handle"); } DWORD type = GetFileType(hnd); if (type != FILE_TYPE_PIPE) return 0; DWORD ret = WaitForSingleObject(hnd, 0); if (ret == WAIT_TIMEOUT) return 0; if (ret == WAIT_FAILED) R_THROW_SYSTEM_ERROR("Cannot poll stdin"); if (ret == WAIT_OBJECT_0) { DWORD avail; DWORD ret2 = PeekNamedPipe(hnd, NULL, 0, NULL, &avail, NULL); if (!ret2) { DWORD err = GetLastError(); if (err == ERROR_BROKEN_PIPE) return 1; R_THROW_SYSTEM_ERROR_CODE(err, "Cannot peek stdin pipe"); } } return 0; } #else int check_stdin(void) { static char buffer[4096]; struct pollfd pfd; pfd.fd = 0; pfd.events = POLLIN; pfd.revents = 0; int ret = poll(&pfd, 1, 0); if (ret == -1) R_THROW_SYSTEM_ERROR("Cannot poll stdin"); // Nothing happened on stdin, keep running if (ret == 0) return 0; // Event on stdin, if we cannot read, then it if EOF ssize_t num = read(0, buffer, 4096); if (num == -1) R_THROW_SYSTEM_ERROR("Cannot read from stdin"); if (num == 0) return 1; // Otherwise fine return 0; } #endif #define WEBFAKES_NOTHING 0 #define WEBFAKES_REQ 1 /* request just came it */ #define WEBFAKES_WAIT 2 /* waited a bit / wait a bit */ #define WEBFAKES_DONE 3 /* request is done */ struct server_user_data { SEXP requests; pthread_cond_t process_more; /* there is something to process */ pthread_cond_t process_less; /* we can process something */ pthread_mutex_t process_lock; struct mg_connection *nextconn; struct mg_server_port ports[4]; int num_ports; int shutdown; }; struct connection_user_data { pthread_cond_t finish_cond; /* can finish callback? */ pthread_mutex_t finish_lock; int main_todo; /* what should the main thread do? */ int req_todo; /* what shoudl the request thread do? */ double secs; /* how much should we wait? */ SEXP req; int id; }; SEXP webfakes_create_request(struct mg_connection *conn); #define PTHCHK(expr) if ((ret = expr)) { \ mg_cry(conn, "ERROR @ %s %s:%d", __func__, __FILE__, __LINE__); \ R_THROW_SYSTEM_ERROR_CODE( \ ret, "Cannot process webfakes web server requests"); \ } #define CHK(expr) if ((ret = expr) < 0) { \ mg_cry(conn, "ERROR @ %s %s:%d", __func__, __FILE__, __LINE__); \ R_THROW_ERROR("Cannot process webfakes web server requests"); \ } static R_INLINE SEXP new_env(void) { SEXP env; #if R_VERSION >= R_Version(4, 1, 0) PROTECT(env = R_NewEnv(R_EmptyEnv, 1, 29)); #else PROTECT(env = allocSExp(ENVSXP)); SET_FRAME(env, R_NilValue); SET_ENCLOS(env, R_EmptyEnv); SET_HASHTAB(env, R_NilValue); SET_ATTRIB(env, R_NilValue); #endif UNPROTECT(1); return env; } static void SEXP_to_char_vector(SEXP x, char*** vec) { int i, len = LENGTH(x); SEXP nms = PROTECT(getAttrib(x, R_NamesSymbol)); *vec = (char**) R_alloc(2 * len + 1, sizeof(char*)); for (i = 0; i < len; i++) { (*vec)[2 * i ] = (char*) CHAR(STRING_ELT(nms, i)); (*vec)[2 * i + 1] = (char*) CHAR(STRING_ELT(x, i)); } (*vec)[2 * len] = NULL; UNPROTECT(1); } static int ms_sleep(struct server_user_data* srv_data, int ms) { int tosleep = ms > 100 ? 100 : ms; while (tosleep > 0) { #ifdef _WIN32 Sleep(tosleep); #else usleep(tosleep * 1000); #endif if (srv_data->shutdown) return 1; ms -= tosleep; tosleep = ms > 100 ? 100 : ms; } return 0; } /* --------------------------------------------------------------------- */ /* civetweb callbacks */ /* --------------------------------------------------------------------- */ static int begin_request(struct mg_connection *conn) { #ifndef NDEBUG fprintf(stderr, "conn %p: starting\n", (void*) conn); #endif struct mg_context *ctx = mg_get_context(conn); struct server_user_data *srv_data = mg_get_user_data(ctx); if (srv_data->shutdown) return 1; struct connection_user_data conn_data = { PTHREAD_COND_INITIALIZER, PTHREAD_MUTEX_INITIALIZER, WEBFAKES_REQ, WEBFAKES_NOTHING, 0.0, R_NilValue, 0, }; mg_set_user_connection_data(conn, &conn_data); if (pthread_mutex_lock(&conn_data.finish_lock)) goto exit; while (1) { if (pthread_mutex_lock(&srv_data->process_lock)) goto exit; #ifndef NDEBUG fprintf(stderr, "conn %p: waiting for slot\n", (void*) conn); #endif while (srv_data->nextconn != NULL) { pthread_cond_wait(&srv_data->process_less, &srv_data->process_lock); } #ifndef NDEBUG fprintf(stderr, "conn %p: scheduled\n", (void*) conn); #endif srv_data->nextconn = conn; /* only used to pass it to the main thread */ if (srv_data->shutdown) goto exit; if (pthread_cond_signal(&srv_data->process_more)) goto exit; if (pthread_mutex_unlock(&srv_data->process_lock)) goto exit; /* Need to wait for the response... */ #ifndef NDEBUG fprintf(stderr, "conn %p: waiting for order\n", (void*) conn); #endif while (conn_data.req_todo == WEBFAKES_NOTHING) { if (pthread_cond_wait(&conn_data.finish_cond, &conn_data.finish_lock)) { goto exit; } } #ifndef NDEBUG fprintf(stderr, "conn %p: got order: %d\n", (void*) conn, conn_data.req_todo); #endif if (conn_data.req_todo == WEBFAKES_DONE) goto exit; if (conn_data.req_todo == WEBFAKES_WAIT) { #ifndef NDEBUG fprintf(stderr, "conn %p: sleeping\n", (void*) conn); #endif if (ms_sleep(srv_data, conn_data.secs * 1000)) goto exit; #ifndef NDEBUG fprintf(stderr, "conn %p: sleeping done\n", (void*) conn); #endif } if (srv_data->shutdown) goto exit; conn_data.main_todo = WEBFAKES_WAIT; conn_data.req_todo = WEBFAKES_NOTHING; } exit: #ifndef NDEBUG fprintf(stderr, "conn %p: good bye all\n", (void*) conn); #endif mg_set_user_connection_data(conn, NULL); pthread_mutex_unlock(&conn_data.finish_lock); pthread_mutex_destroy(&conn_data.finish_lock); pthread_cond_destroy(&conn_data.finish_cond); return 1; } /* --------------------------------------------------------------------- */ /* server */ /* --------------------------------------------------------------------- */ static int register_request(struct server_user_data *srv_data, SEXP req) { SEXP nextid = PROTECT(Rf_install("nextid")); int id = INTEGER(Rf_findVar(nextid, srv_data->requests))[0] + 1; SEXP xid = PROTECT(ScalarInteger(id)); Rf_defineVar(nextid, xid, srv_data->requests); SEXP cid = PROTECT(asChar(xid)); SEXP rname = PROTECT(Rf_installChar(cid)); Rf_defineVar(rname, req, srv_data->requests); UNPROTECT(4); return id; } static void deregister_request(struct server_user_data *srv_data, int id) { SEXP xid = PROTECT(ScalarInteger(id)); SEXP cid = PROTECT(asChar(xid)); SEXP rname = PROTECT(Rf_installChar(cid)); Rf_defineVar(rname, R_NilValue, srv_data->requests); UNPROTECT(3); } static void release_all_requests(SEXP requests) { SEXP nms = PROTECT(R_lsInternal3(requests, 1, 0)); int i, n = LENGTH(nms); for (i = 0; i < n; i++) { const char *nm = CHAR(STRING_ELT(nms, i)); if (!strcmp("nextid", nm)) continue; SEXP sym = PROTECT(Rf_installChar(STRING_ELT(nms, i))); SEXP req = Rf_findVar(sym, requests); if (!isNull(req)) { SEXP xconn = Rf_findVar(Rf_install(".xconn"), req); struct mg_connection *conn = R_ExternalPtrAddr(xconn); if (conn) { #ifndef NDEBUG fprintf(stderr, "conn %p: emergency cleanup\n", (void*) conn); #endif struct connection_user_data *conn_data = mg_get_user_connection_data(conn); struct mg_context *ctx = mg_get_context(conn); struct server_user_data *srv_data = mg_get_user_data(ctx); pthread_mutex_lock(&conn_data->finish_lock); conn_data->req_todo = WEBFAKES_DONE; conn_data->req = R_NilValue; pthread_cond_signal(&conn_data->finish_cond); pthread_mutex_unlock(&conn_data->finish_lock); pthread_cond_signal(&srv_data->process_less); } } UNPROTECT(1); } UNPROTECT(1); } static void webfakes_server_finalizer(SEXP server) { struct mg_context *ctx = R_ExternalPtrAddr(server); if (ctx == NULL) return; #ifndef NDEBUG fprintf(stderr, "serv %p: cleaning up\n", (void*) ctx); #endif R_ClearExternalPtr(server); struct server_user_data* srv_data = mg_get_user_data(ctx); srv_data->shutdown = 1; release_all_requests(srv_data->requests); #ifndef NDEBUG fprintf(stderr, "serv %p: waiting for worker threads\n", (void*) ctx); void *ctx_addr = ctx; #endif mg_stop(ctx); pthread_mutex_unlock(&srv_data->process_lock); pthread_mutex_destroy(&srv_data->process_lock); pthread_cond_destroy(&srv_data->process_more); pthread_cond_destroy(&srv_data->process_less); free(srv_data); #ifndef NDEBUG fprintf(stderr, "serv %p: that would be all for today\n", (void*) ctx_addr); #endif } SEXP server_start(SEXP options) { #ifndef NDEBUG fprintf(stderr, "creating new server\n"); #endif SEXP server = R_NilValue; struct server_user_data *srv_data = malloc(sizeof(struct server_user_data)); if (!srv_data) R_THROW_SYSTEM_ERROR("Cannot start webfakes server"); struct mg_context *ctx = NULL; int ret = 0; memset(srv_data, 0, sizeof(struct server_user_data)); srv_data->requests = PROTECT(new_env()); SEXP x1 = PROTECT(ScalarInteger(1)); Rf_defineVar(Rf_install("nextid"), x1, srv_data->requests); UNPROTECT(1); if ((ret = pthread_cond_init(&srv_data->process_more, NULL))) goto cleanup; if ((ret = pthread_cond_init(&srv_data->process_less, NULL))) goto cleanup; if ((ret = pthread_mutex_init(&srv_data->process_lock, NULL))) goto cleanup; char **coptions; SEXP_to_char_vector(options, &coptions); struct mg_callbacks callbacks; memset(&callbacks, 0, sizeof(callbacks)); callbacks.begin_request = begin_request; if ((ret = pthread_mutex_lock(&srv_data->process_lock))) goto cleanup; ctx = mg_start(&callbacks, srv_data, (const char **) coptions); if (ctx == NULL) goto cleanup; PROTECT(server = R_MakeExternalPtr(ctx, srv_data->requests, R_NilValue)); R_RegisterCFinalizer(server, webfakes_server_finalizer); #ifndef NDEBUG fprintf(stderr, "serv %p: hi everyone, ready to serve\n", (void*) ctx); #endif memset(srv_data->ports, 0, sizeof(srv_data->ports)); srv_data->num_ports = mg_get_server_ports( ctx, sizeof(srv_data->ports) / sizeof(struct mg_server_port), srv_data->ports ); if (srv_data->num_ports < 0) goto cleanup; UNPROTECT(2); return server; cleanup: #ifndef NDEBUG fprintf(stderr, "serv %p: failed to start new server\n", (void*) ctx); #endif /* This is unlocked in the finalizer, but that might be much later... */ if (ctx) mg_stop(ctx); pthread_mutex_unlock(&srv_data->process_lock); if (ret) { R_THROW_SYSTEM_ERROR_CODE(ret, "Cannot start webfakes web server"); } else { R_THROW_ERROR("Cannot start webfakes web server"); } /* Never reached */ return R_NilValue; } static void server_poll_cleanup(void *ptr) { #ifndef NDEBUG fprintf(stderr, "conn %p: oh-oh, forced cleanup\n", (void*) ptr); #endif struct mg_connection *conn = (struct mg_connection*) ptr; struct connection_user_data *conn_data = mg_get_user_connection_data(conn); struct mg_context *ctx = mg_get_context(conn); struct server_user_data *srv_data = mg_get_user_data(ctx); mg_cry(conn, "Cleaning up broken connection at %s:%d", __FILE__, __LINE__); pthread_mutex_lock(&conn_data->finish_lock); conn_data->req_todo = WEBFAKES_DONE; deregister_request(srv_data, conn_data->id); conn_data->req = R_NilValue; pthread_cond_signal(&conn_data->finish_cond); pthread_mutex_unlock(&conn_data->finish_lock); pthread_cond_signal(&srv_data->process_less); } SEXP server_poll(SEXP server, SEXP clean) { struct mg_context *ctx = R_ExternalPtrAddr(server); int cclean = LOGICAL(clean)[0]; #ifndef NDEBUG fprintf(stderr, "serv %p: polling\n", (void*) ctx); #endif if (ctx == NULL) R_THROW_ERROR("webfakes server has stopped already"); struct server_user_data *srv_data = mg_get_user_data(ctx); struct timespec limit; while (srv_data->nextconn == NULL) { webfakes_clock_gettime(CLOCK_REALTIME, &limit); limit.tv_nsec += 50 * 1000 * 1000; if (limit.tv_nsec >= 1000 * 1000 * 1000) { limit.tv_sec += 1; limit.tv_nsec %= 1000 * 1000 * 1000; } R_CheckUserInterrupt(); if (cclean && check_stdin()) R_THROW_ERROR("Cleaning up web server"); (void) pthread_cond_timedwait(&srv_data->process_more, &srv_data->process_lock, &limit); } struct mg_connection *conn = srv_data->nextconn; srv_data->nextconn = NULL; struct connection_user_data *conn_data = mg_get_user_connection_data(conn); #ifndef NDEBUG fprintf(stderr, "serv %p: processing conn %p\n", (void*) ctx, (void*) conn); #endif SEXP req = R_NilValue; switch(conn_data->main_todo) { case WEBFAKES_REQ: r_call_on_early_exit(server_poll_cleanup, conn); req = webfakes_create_request(conn); break; case WEBFAKES_WAIT: req = conn_data->req; break; default: break; } #ifndef NDEBUG fprintf(stderr, "serv %p: returning request from conn %p\n", (void*) ctx, (void*) conn); #endif return req; } SEXP server_stop(SEXP server) { webfakes_server_finalizer(server); return R_NilValue; } SEXP server_get_ports(SEXP server) { struct mg_context *ctx = R_ExternalPtrAddr(server); if (ctx == NULL) R_THROW_ERROR("webfakes server has stopped already"); struct server_user_data *srv_data = mg_get_user_data(ctx); int i, num_ports = srv_data->num_ports; SEXP ipv4 = PROTECT(allocVector(LGLSXP, num_ports)); SEXP ipv6 = PROTECT(allocVector(LGLSXP, num_ports)); SEXP port = PROTECT(allocVector(INTSXP, num_ports)); SEXP ssl = PROTECT(allocVector(LGLSXP, num_ports)); const char *res_names[] = { "ipv4", "ipv6", "port", "ssl", "" }; SEXP res = PROTECT(Rf_mkNamed(VECSXP, res_names)); for (i = 0; i < num_ports; i++) { LOGICAL(ipv4)[i] = (srv_data->ports[i].protocol) & 1; LOGICAL(ipv6)[i] = (srv_data->ports[i].protocol) & 2; INTEGER(port)[i] = srv_data->ports[i].port; LOGICAL(ssl )[i] = srv_data->ports[i].is_ssl == 1; } SET_VECTOR_ELT(res, 0, ipv4); SET_VECTOR_ELT(res, 1, ipv6); SET_VECTOR_ELT(res, 2, port); SET_VECTOR_ELT(res, 3, ssl); UNPROTECT(5); return res; } /* --------------------------------------------------------------------- */ /* request */ /* --------------------------------------------------------------------- */ SEXP webfakes_create_request(struct mg_connection *conn) { static char request_link[8192]; int i; SEXP x; #ifndef NDEBUG fprintf(stderr, "conn %p: creating an R request object\n", (void*) conn); #endif const struct mg_request_info *req_info = mg_get_request_info(conn); SEXP req = PROTECT(new_env()); x = PROTECT(mkString(req_info->request_method)); defineVar(Rf_install("method"), x, req); UNPROTECT(1); mg_get_request_link(conn, request_link, sizeof(request_link)); x = PROTECT(mkString(request_link)); defineVar(Rf_install("url"), x, req); UNPROTECT(1); x = PROTECT(mkString(req_info->request_uri)); defineVar(Rf_install("request_uri"), x, req); UNPROTECT(1); x = PROTECT(mkString(req_info->local_uri)); defineVar(Rf_install("path"), x, req); UNPROTECT(1); x = PROTECT(mkString(req_info->http_version)); defineVar(Rf_install("http_version"), x, req); UNPROTECT(1); x = PROTECT(req_info->query_string ? mkString(req_info->query_string) : mkString("")); defineVar(Rf_install("query_string"), x, req); UNPROTECT(1); x = PROTECT(mkString(req_info->remote_addr)); defineVar(Rf_install("remote_addr"), x, req); UNPROTECT(1); x = PROTECT(ScalarReal(req_info->content_length)); defineVar(Rf_install("content_length"), x, req); UNPROTECT(1); x = PROTECT(ScalarInteger(req_info->remote_port)); defineVar(Rf_install("remote_port"), x, req); UNPROTECT(1); SEXP hdr = PROTECT(allocVector(VECSXP, req_info->num_headers)); SEXP nms = PROTECT(allocVector(STRSXP, req_info->num_headers)); for (i = 0; i < req_info->num_headers; i++) { SET_VECTOR_ELT(hdr, i, mkString(req_info->http_headers[i].value)); SET_STRING_ELT(nms, i, mkChar(req_info->http_headers[i].name)); } Rf_setAttrib(hdr, R_NamesSymbol, nms); defineVar(Rf_install("headers"), hdr, req); if (req_info->content_length != -1) { SEXP body = PROTECT(allocVector(RAWSXP, req_info->content_length)); int ret = mg_read(conn, RAW(body), req_info->content_length); if (ret < 0) { mg_cry(conn, "ERROR @ %s %s:%d", __func__, __FILE__, __LINE__); R_THROW_ERROR("Cannot read from webfakes HTTP client"); } if (ret != req_info->content_length) { warning("Partial HTTP request body from client"); } defineVar(Rf_install(".body"), body, req); UNPROTECT(1); } else { defineVar(Rf_install(".body"), R_NilValue, req); } SEXP xreq = PROTECT(R_MakeExternalPtr(conn, R_NilValue, R_NilValue)); defineVar(Rf_install(".xconn"), xreq, req); UNPROTECT(1); struct connection_user_data *conn_data = mg_get_user_connection_data(conn); conn_data->req = req; struct mg_context *ctx = mg_get_context(conn); struct server_user_data *srv_data = mg_get_user_data(ctx); conn_data->id = register_request(srv_data, req); UNPROTECT(3); return req; } static void response_cleanup(void *ptr) { struct mg_connection *conn = (struct mg_connection*) ptr; struct connection_user_data *conn_data = mg_get_user_connection_data(conn); struct mg_context *ctx = mg_get_context(conn); struct server_user_data *srv_data = mg_get_user_data(ctx); if (conn_data) { #ifndef NDEBUG fprintf(stderr, "conn %p: oh-oh, emergency cleanup\n", (void*) ptr); #endif mg_set_user_connection_data(conn, NULL); mg_cry(conn, "Cleaning up broken connection %p at %s:%d", (void*) conn, __FILE__, __LINE__); pthread_mutex_lock(&conn_data->finish_lock); conn_data->req_todo = WEBFAKES_DONE; deregister_request(srv_data, conn_data->id); SEXP req = conn_data->req; SEXP xconn = Rf_findVar(Rf_install(".xconn"), req); R_ClearExternalPtr(xconn); conn_data->req = R_NilValue; pthread_cond_signal(&conn_data->finish_cond); pthread_mutex_unlock(&conn_data->finish_lock); } pthread_cond_signal(&srv_data->process_less); } SEXP response_delay(SEXP req, SEXP secs) { SEXP xconn = Rf_findVar(Rf_install(".xconn"), req); struct mg_connection *conn = R_ExternalPtrAddr(xconn); if (conn == 0) { #ifndef NDEBUG fprintf(stderr, "?? a connection was cleaned up already\n"); return R_NilValue; #endif } struct mg_context *ctx = mg_get_context(conn); #ifndef NDEBUG fprintf(stderr, "serv %p: telling conn %p to delay\n", (void*) ctx, (void*) conn); #endif struct connection_user_data *conn_data = mg_get_user_connection_data(conn); int ret; r_call_on_early_exit(response_cleanup, conn); pthread_mutex_lock(&conn_data->finish_lock); conn_data->secs = REAL(secs)[0]; conn_data->req_todo = WEBFAKES_WAIT; PTHCHK(pthread_cond_signal(&conn_data->finish_cond)); PTHCHK(pthread_mutex_unlock(&conn_data->finish_lock)); struct server_user_data *srv_data = mg_get_user_data(ctx); #ifndef NDEBUG fprintf(stderr, "serv %p: inviting request threads\n", (void*) ctx); #endif PTHCHK(pthread_cond_signal(&srv_data->process_less)); return R_NilValue; } SEXP response_send_headers(SEXP req) { SEXP xconn = Rf_findVar(Rf_install(".xconn"), req); struct mg_connection *conn = R_ExternalPtrAddr(xconn); if (conn == 0) { #ifndef NDEBUG fprintf(stderr, "?? a connection was cleaned up already\n"); return R_NilValue; #endif } #ifndef NDEBUG fprintf(stderr, "conn %p: sending response headers\n", (void*) conn); #endif r_call_on_early_exit(response_cleanup, conn); SEXP http_version = PROTECT(Rf_findVar(Rf_install("http_version"), req)); SEXP res = PROTECT(Rf_findVar(Rf_install("res"), req)); SEXP headers = PROTECT(Rf_findVar(Rf_install(".headers"), res)); SEXP names = PROTECT(Rf_getAttrib(headers, R_NamesSymbol)); SEXP status = PROTECT(Rf_findVar(Rf_install(".status"), res)); int ret, i, nh = isNull(headers) ? 0 : LENGTH(headers); CHK(mg_printf(conn, "HTTP/%s %d %s\r\n", CHAR(STRING_ELT(http_version, 0)), INTEGER(status)[0], mg_get_response_code_text(conn, INTEGER(status)[0]))); for (i = 0; i < nh; i++) { const char *k = CHAR(STRING_ELT(names, i)); const char *v = CHAR(STRING_ELT(VECTOR_ELT(headers, i), 0)); CHK(mg_printf(conn, "%s: %s\r\n", k, v)); } CHK(mg_printf(conn, "\r\n")); #ifndef NDEBUG fprintf(stderr, "conn %p: response headers sent\n", (void*) conn); #endif UNPROTECT(5); return R_NilValue; } SEXP response_send(SEXP req) { SEXP xconn = Rf_findVar(Rf_install(".xconn"), req); struct mg_connection *conn = R_ExternalPtrAddr(xconn); if (conn == 0) { #ifndef NDEBUG fprintf(stderr, "?? a connection was cleaned up already\n"); return R_NilValue; #endif } #ifndef NDEBUG fprintf(stderr, "conn %p: sending response body\n", (void*) conn); #endif SEXP res = PROTECT(Rf_findVar(Rf_install("res"), req)); SEXP headers_sent = Rf_findVar(Rf_install("headers_sent"), res); if (! LOGICAL(headers_sent)[0]) response_send_headers(req); struct connection_user_data *conn_data = mg_get_user_connection_data(conn); r_call_on_early_exit(response_cleanup, conn); SEXP body = Rf_findVar(Rf_install(".body"), res); int ret; if (TYPEOF(body) == RAWSXP) { CHK(mg_write(conn, RAW(body), LENGTH(body))); } else if (TYPEOF(body) == STRSXP) { const char *cbody = CHAR(STRING_ELT(body, 0)); CHK(mg_write(conn, cbody, strlen(cbody))); } struct mg_context *ctx = mg_get_context(conn); struct server_user_data *srv_data = mg_get_user_data(ctx); #ifndef NDEBUG fprintf(stderr, "conn %p: response body sent\n", (void*) conn); #endif pthread_mutex_lock(&conn_data->finish_lock); conn_data->req_todo = WEBFAKES_DONE; deregister_request(srv_data, conn_data->id); conn_data->req = R_NilValue; #ifndef NDEBUG fprintf(stderr, "serv %p: telling conn %p to quit\n", (void*) ctx, (void*) conn); #endif PTHCHK(pthread_cond_signal(&conn_data->finish_cond)); PTHCHK(pthread_mutex_unlock(&conn_data->finish_lock)); #ifndef NDEBUG fprintf(stderr, "serv %p: inviting request threads\n", (void*) ctx); #endif PTHCHK(pthread_cond_signal(&srv_data->process_less)); UNPROTECT(1); return R_NilValue; } SEXP response_write(SEXP req, SEXP data) { SEXP res = PROTECT(Rf_findVar(Rf_install("res"), req)); SEXP headers_sent = PROTECT(Rf_findVar(Rf_install("headers_sent"), res)); if (! LOGICAL(headers_sent)[0]) response_send_headers(req); SEXP xconn = Rf_findVar(Rf_install(".xconn"), req); struct mg_connection *conn = R_ExternalPtrAddr(xconn); if (conn == 0) { #ifndef NDEBUG fprintf(stderr, "?? a connection was cleaned up already\n"); return R_NilValue; #endif } r_call_on_early_exit(response_cleanup, conn); int ret = 0; int len = LENGTH(data); #ifndef NDEBUG fprintf(stderr, "conn %p: writing %d bytes\n", (void*) conn, len); #endif CHK(mg_write(conn, RAW(data), len)); UNPROTECT(2); return R_NilValue; } SEXP response_send_chunk(SEXP req, SEXP data) { SEXP res = PROTECT(Rf_findVar(Rf_install("res"), req)); SEXP headers_sent = PROTECT(Rf_findVar(Rf_install("headers_sent"), res)); if (! LOGICAL(headers_sent)[0]) response_send_headers(req); SEXP xconn = Rf_findVar(Rf_install(".xconn"), req); struct mg_connection *conn = R_ExternalPtrAddr(xconn); if (conn == 0) { #ifndef NDEBUG fprintf(stderr, "?? a connection was cleaned up already\n"); return R_NilValue; #endif } r_call_on_early_exit(response_cleanup, conn); int ret = 0; int len = LENGTH(data); #ifndef NDEBUG fprintf(stderr, "conn %p: sending chunk of %d bytes\n", (void*) conn, len); #endif CHK(mg_send_chunk(conn, (const char*) RAW(data), len)); UNPROTECT(2); return R_NilValue; } SEXP response_send_error(SEXP req, SEXP message, SEXP status) { #ifndef NDEBUG fprintf(stderr, "sending 500 response"); #endif SEXP res = PROTECT(Rf_findVar(Rf_install("res"), req)); Rf_defineVar(Rf_install(".body"), message, res); Rf_defineVar(Rf_install(".status"), status, res); UNPROTECT(1); return response_send(req); } webfakes/src/timer.h0000644000176200001440000001631714740430263014134 0ustar liggesusers/* This file is part of the CivetWeb web server. * See https://github.com/civetweb/civetweb/ * (C) 2014-2021 by the CivetWeb authors, MIT license. */ #if !defined(MAX_TIMERS) #define MAX_TIMERS MAX_WORKER_THREADS #endif #if !defined(TIMER_RESOLUTION) /* Timer resolution in ms */ #define TIMER_RESOLUTION (10) #endif typedef int (*taction)(void *arg); typedef void (*tcancelaction)(void *arg); struct ttimer { double time; double period; taction action; void *arg; tcancelaction cancel; }; struct ttimers { pthread_t threadid; /* Timer thread ID */ pthread_mutex_t mutex; /* Protects timer lists */ struct ttimer *timers; /* List of timers */ unsigned timer_count; /* Current size of timer list */ unsigned timer_capacity; /* Capacity of timer list */ #if defined(_WIN32) DWORD last_tick; uint64_t now_tick64; #endif }; TIMER_API double timer_getcurrenttime(struct mg_context *ctx) { #if defined(_WIN32) /* GetTickCount returns milliseconds since system start as * unsigned 32 bit value. It will wrap around every 49.7 days. * We need to use a 64 bit counter (will wrap in 500 mio. years), * by adding the 32 bit difference since the last call to a * 64 bit counter. This algorithm will only work, if this * function is called at least once every 7 weeks. */ uint64_t now_tick64 = 0; DWORD now_tick = GetTickCount(); if (ctx->timers) { pthread_mutex_lock(&ctx->timers->mutex); ctx->timers->now_tick64 += now_tick - ctx->timers->last_tick; now_tick64 = ctx->timers->now_tick64; ctx->timers->last_tick = now_tick; pthread_mutex_unlock(&ctx->timers->mutex); } return (double)now_tick64 * 1.0E-3; #else struct timespec now_ts; (void)ctx; clock_gettime(CLOCK_MONOTONIC, &now_ts); return (double)now_ts.tv_sec + (double)now_ts.tv_nsec * 1.0E-9; #endif } TIMER_API int timer_add(struct mg_context *ctx, double next_time, double period, int is_relative, taction action, void *arg, tcancelaction cancel) { int error = 0; double now; if (!ctx->timers) { return 1; } now = timer_getcurrenttime(ctx); /* HCP24: if is_relative = 0 and next_time < now * action will be called so fast as possible * if additional period > 0 * action will be called so fast as possible * n times until (next_time + (n * period)) > now * then the period is working * Solution: * if next_time < now then we set next_time = now. * The first callback will be so fast as possible (now) * but the next callback on period */ if (is_relative) { next_time += now; } /* You can not set timers into the past */ if (next_time < now) { next_time = now; } pthread_mutex_lock(&ctx->timers->mutex); if (ctx->timers->timer_count == MAX_TIMERS) { error = 1; } else if (ctx->timers->timer_count == ctx->timers->timer_capacity) { unsigned capacity = (ctx->timers->timer_capacity * 2) + 1; struct ttimer *timers = (struct ttimer *)mg_realloc_ctx(ctx->timers->timers, capacity * sizeof(struct ttimer), ctx); if (timers) { ctx->timers->timers = timers; ctx->timers->timer_capacity = capacity; } else { error = 1; } } if (!error) { /* Insert new timer into a sorted list. */ /* The linear list is still most efficient for short lists (small * number of timers) - if there are many timers, different * algorithms will work better. */ unsigned u = ctx->timers->timer_count; for (; (u > 0) && (ctx->timers->timers[u - 1].time > next_time); u--) { ctx->timers->timers[u] = ctx->timers->timers[u - 1]; } ctx->timers->timers[u].time = next_time; ctx->timers->timers[u].period = period; ctx->timers->timers[u].action = action; ctx->timers->timers[u].arg = arg; ctx->timers->timers[u].cancel = cancel; ctx->timers->timer_count++; } pthread_mutex_unlock(&ctx->timers->mutex); return error; } static void timer_thread_run(void *thread_func_param) { struct mg_context *ctx = (struct mg_context *)thread_func_param; double d; unsigned u; int action_res; struct ttimer t; mg_set_thread_name("timer"); if (ctx->callbacks.init_thread) { /* Timer thread */ ctx->callbacks.init_thread(ctx, 2); } /* Timer main loop */ d = timer_getcurrenttime(ctx); while (STOP_FLAG_IS_ZERO(&ctx->stop_flag)) { pthread_mutex_lock(&ctx->timers->mutex); if ((ctx->timers->timer_count > 0) && (d >= ctx->timers->timers[0].time)) { /* Timer list is sorted. First action should run now. */ /* Store active timer in "t" */ t = ctx->timers->timers[0]; /* Shift all other timers */ for (u = 1; u < ctx->timers->timer_count; u++) { ctx->timers->timers[u - 1] = ctx->timers->timers[u]; } ctx->timers->timer_count--; pthread_mutex_unlock(&ctx->timers->mutex); /* Call timer action */ action_res = t.action(t.arg); /* action_res == 1: reschedule */ /* action_res == 0: do not reschedule, free(arg) */ if ((action_res > 0) && (t.period > 0)) { /* Should schedule timer again */ timer_add(ctx, t.time + t.period, t.period, 0, t.action, t.arg, t.cancel); } else { /* Allow user to free timer argument */ if (t.cancel != NULL) { t.cancel(t.arg); } } continue; } else { pthread_mutex_unlock(&ctx->timers->mutex); } /* TIMER_RESOLUTION = 10 ms seems reasonable. * A faster loop (smaller sleep value) increases CPU load, * a slower loop (higher sleep value) decreases timer accuracy. */ mg_sleep(TIMER_RESOLUTION); d = timer_getcurrenttime(ctx); } /* Remove remaining timers */ for (u = 0; u < ctx->timers->timer_count; u++) { t = ctx->timers->timers[u]; if (t.cancel != NULL) { t.cancel(t.arg); } } } #if defined(_WIN32) static unsigned __stdcall timer_thread(void *thread_func_param) { timer_thread_run(thread_func_param); return 0; } #else static void * timer_thread(void *thread_func_param) { struct sigaction sa; /* Ignore SIGPIPE */ memset(&sa, 0, sizeof(sa)); sa.sa_handler = SIG_IGN; sigaction(SIGPIPE, &sa, NULL); timer_thread_run(thread_func_param); return NULL; } #endif /* _WIN32 */ TIMER_API int timers_init(struct mg_context *ctx) { /* Initialize timers data structure */ ctx->timers = (struct ttimers *)mg_calloc_ctx(sizeof(struct ttimers), 1, ctx); if (!ctx->timers) { return -1; } ctx->timers->timers = NULL; /* Initialize mutex */ if (0 != pthread_mutex_init(&ctx->timers->mutex, NULL)) { mg_free(ctx->timers); ctx->timers = NULL; return -1; } /* For some systems timer_getcurrenttime does some initialization * during the first call. Call it once now, ignore the result. */ (void)timer_getcurrenttime(ctx); /* Start timer thread */ if (mg_start_thread_with_id(timer_thread, ctx, &ctx->timers->threadid) != 0) { (void)pthread_mutex_destroy(&ctx->timers->mutex); mg_free(ctx->timers); ctx->timers = NULL; return -1; } return 0; } TIMER_API void timers_exit(struct mg_context *ctx) { if (ctx->timers) { mg_join_thread(ctx->timers->threadid); (void)pthread_mutex_destroy(&ctx->timers->mutex); mg_free(ctx->timers->timers); mg_free(ctx->timers); ctx->timers = NULL; } } /* End of timer.inl */ webfakes/src/Makevars.win0000644000176200001440000000011014740430263015113 0ustar liggesusers PKG_CPPFLAGS=-DNO_SSL PKG_CFLAGS=-DNO_CGI -DNO_FILES PKG_LIBS=-lws2_32 webfakes/src/errors.c0000644000176200001440000000415614740243712014322 0ustar liggesusers #include "errors.h" #include #include #define ERRORBUF_SIZE 4096 static char errorbuf[ERRORBUF_SIZE]; SEXP r_throw_error(const char *func, const char *filename, int line, const char *msg, ...) { va_list args; errorbuf[0] = '\0'; va_start(args, msg); vsnprintf(errorbuf, ERRORBUF_SIZE, msg, args); va_end (args); error("%s @%s:%d (%s)", errorbuf, filename, line, func); return R_NilValue; } #ifdef _WIN32 SEXP r_throw_system_error(const char *func, const char *filename, int line, DWORD errorcode, const char *sysmsg, const char *msg, ...) { va_list args; LPVOID lpMsgBuf; char *realsysmsg = sysmsg ? (char*) sysmsg : NULL; char *failmsg = "Formatting the system message failed :("; if (errorcode == -1) errorcode = GetLastError(); if (!realsysmsg) { DWORD ret = FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, errorcode, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &lpMsgBuf, 0, NULL); if (ret == 0) { realsysmsg = failmsg; } else { realsysmsg = R_alloc(1, strlen(lpMsgBuf) + 1); strcpy(realsysmsg, lpMsgBuf); LocalFree(lpMsgBuf); } } errorbuf[0] = '\0'; va_start(args, msg); vsnprintf(errorbuf, ERRORBUF_SIZE, msg, args); va_end(args); error("%s (system error %ld, %s) @%s:%d (%s)", errorbuf, errorcode, realsysmsg, filename, line, func); return R_NilValue; } #endif #ifdef _WIN32 SEXP r_throw_posix_error( #else SEXP r_throw_system_error( #endif const char *func, const char *filename, int line, int errorcode, const char *sysmsg, const char *msg, ...) { va_list args; if (!sysmsg) sysmsg = strerror(errorcode); errorbuf[0] = '\0'; va_start(args, msg); vsnprintf(errorbuf, ERRORBUF_SIZE, msg, args); va_end(args); error("%s (system error %d, %s) @%s:%d (%s)", errorbuf, errorcode, sysmsg, filename, line, func); return R_NilValue; } webfakes/src/civetweb.h0000644000176200001440000020542614740430263014625 0ustar liggesusers/* Copyright (c) 2013-2021 the Civetweb developers * Copyright (c) 2004-2013 Sergey Lyubka * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef CIVETWEB_HEADER_INCLUDED #define CIVETWEB_HEADER_INCLUDED #define CIVETWEB_VERSION "1.16" #define CIVETWEB_VERSION_MAJOR (1) #define CIVETWEB_VERSION_MINOR (16) #define CIVETWEB_VERSION_PATCH (0) #ifndef CIVETWEB_API #if defined(_WIN32) #if defined(CIVETWEB_DLL_EXPORTS) #define CIVETWEB_API __declspec(dllexport) #elif defined(CIVETWEB_DLL_IMPORTS) #define CIVETWEB_API __declspec(dllimport) #else #define CIVETWEB_API #endif #elif __GNUC__ >= 4 #define CIVETWEB_API __attribute__((visibility("default"))) #else #define CIVETWEB_API #endif #endif #include #include #ifdef __cplusplus extern "C" { #endif /* __cplusplus */ /* Init Features */ enum { MG_FEATURES_DEFAULT = 0x0u, /* Support files from local directories */ /* Will only work, if NO_FILES is not set. */ MG_FEATURES_FILES = 0x1u, /* Support transport layer security (TLS). */ /* SSL is still often used synonymously for TLS. */ /* Will only work, if NO_SSL is not set. */ MG_FEATURES_TLS = 0x2u, MG_FEATURES_SSL = 0x2u, /* Support common gateway interface (CGI). */ /* Will only work, if NO_CGI is not set. */ MG_FEATURES_CGI = 0x4u, /* Support IPv6. */ /* Will only work, if USE_IPV6 is set. */ MG_FEATURES_IPV6 = 0x8u, /* Support WebSocket protocol. */ /* Will only work, if USE_WEBSOCKET is set. */ MG_FEATURES_WEBSOCKET = 0x10u, /* Support server side Lua scripting. */ /* Will only work, if USE_LUA is set. */ MG_FEATURES_LUA = 0x20u, /* Support server side JavaScript scripting. */ /* Will only work, if USE_DUKTAPE is set. */ MG_FEATURES_SSJS = 0x40u, /* Provide data required for caching files. */ /* Will only work, if NO_CACHING is not set. */ MG_FEATURES_CACHE = 0x80u, /* Collect server status information. */ /* Will only work, if USE_SERVER_STATS is set. */ MG_FEATURES_STATS = 0x100u, /* Support on-the-fly compression. */ /* Will only work, if USE_ZLIB is set. */ MG_FEATURES_COMPRESSION = 0x200u, /* HTTP/2 support enabled. */ MG_FEATURES_HTTP2 = 0x400u, /* Support unix domain sockets. */ MG_FEATURES_X_DOMAIN_SOCKET = 0x800u, /* Bit mask for all feature defines. */ MG_FEATURES_ALL = 0xFFFFu }; /* Initialize this library. This should be called once before any other * function from this library. This function is not guaranteed to be * thread safe. * Parameters: * features: bit mask for features to be initialized. * Note: The TLS libraries (like OpenSSL) is initialized * only if the MG_FEATURES_TLS bit is set. * Currently the other bits do not influence * initialization, but this may change in future * versions. * Return value: * initialized features * 0: error */ CIVETWEB_API unsigned mg_init_library(unsigned features); /* Un-initialize this library. * Return value: * 0: error */ CIVETWEB_API unsigned mg_exit_library(void); struct mg_context; /* Handle for the HTTP service itself */ struct mg_connection; /* Handle for the individual connection */ /* Maximum number of headers */ #define MG_MAX_HEADERS (64) struct mg_header { const char *name; /* HTTP header name */ const char *value; /* HTTP header value */ }; /* This structure contains information about the HTTP request. */ struct mg_request_info { const char *request_method; /* "GET", "POST", etc */ const char *request_uri; /* URL-decoded URI (absolute or relative, * as in the request) */ const char *local_uri_raw; /* URL-decoded URI (relative). Can be NULL * if the request_uri does not address a * resource at the server host. */ const char *local_uri; /* Same as local_uri_raw, however, cleaned * so a path like * allowed_dir/../forbidden_file * is not possible. */ const char *http_version; /* E.g. "1.0", "1.1" */ const char *query_string; /* URL part after '?', not including '?', or NULL */ const char *remote_user; /* Authenticated user, or NULL if no auth used */ char remote_addr[48]; /* Client's IP address as a string. */ long long content_length; /* Length (in bytes) of the request body, can be -1 if no length was given. */ int remote_port; /* Port at client side */ int server_port; /* Port at server side (one of the listening ports) */ int is_ssl; /* 1 if HTTPS or WS is used (SSL/TLS used), 0 if not */ void *user_data; /* User data pointer passed to mg_start() */ void *conn_data; /* Connection-specific user data */ int num_headers; /* Number of HTTP headers */ struct mg_header http_headers[MG_MAX_HEADERS]; /* Allocate maximum headers */ struct mg_client_cert *client_cert; /* Client certificate information */ const char *acceptedWebSocketSubprotocol; /* websocket subprotocol, * accepted during handshake */ }; /* This structure contains information about the HTTP request. */ /* This structure may be extended in future versions. */ struct mg_response_info { int status_code; /* E.g. 200 */ const char *status_text; /* E.g. "OK" */ const char *http_version; /* E.g. "1.0", "1.1" */ long long content_length; /* Length (in bytes) of the request body, can be -1 if no length was given. */ int num_headers; /* Number of HTTP headers */ struct mg_header http_headers[MG_MAX_HEADERS]; /* Allocate maximum headers */ }; /* Client certificate information (part of mg_request_info) */ struct mg_client_cert { void *peer_cert; const char *subject; const char *issuer; const char *serial; const char *finger; }; /* This structure needs to be passed to mg_start(), to let civetweb know which callbacks to invoke. For a detailed description, see https://github.com/civetweb/civetweb/blob/master/docs/UserManual.md */ struct mg_callbacks { /* Called when civetweb has received new HTTP request. If the callback returns one, it must process the request by sending valid HTTP headers and a body. Civetweb will not do any further processing. Otherwise it must return zero. Note that since V1.7 the "begin_request" function is called before an authorization check. If an authorization check is required, use a request_handler instead. Return value: 0: civetweb will process the request itself. In this case, the callback must not send any data to the client. 1-999: callback already processed the request. Civetweb will not send any data after the callback returned. The return code is stored as a HTTP status code for the access log. */ int (*begin_request)(struct mg_connection *); /* Called when civetweb has finished processing request. */ void (*end_request)(const struct mg_connection *, int reply_status_code); /* Called when civetweb is about to log a message. If callback returns non-zero, civetweb does not log anything. */ int (*log_message)(const struct mg_connection *, const char *message); /* Called when civetweb is about to log access. If callback returns non-zero, civetweb does not log anything. */ int (*log_access)(const struct mg_connection *, const char *message); /* Called when civetweb initializes SSL library. Parameters: ssl_ctx: SSL_CTX pointer. user_data: parameter user_data passed when starting the server. Return value: 0: civetweb will set up the SSL certificate. 1: civetweb assumes the callback already set up the certificate. -1: initializing ssl fails. */ int (*init_ssl)(void *ssl_ctx, void *user_data); /* Called when civetweb initializes SSL library for a domain. Parameters: server_domain: authentication_domain from the domain config. ssl_ctx: SSL_CTX pointer. user_data: parameter user_data passed when starting the server. Return value: 0: civetweb will set up the SSL certificate. 1: civetweb assumes the callback already set up the certificate. -1: initializing ssl fails. */ int (*init_ssl_domain)(const char *server_domain, void *ssl_ctx, void *user_data); /* Called when civetweb is about to create or free a SSL_CTX. Parameters: ssl_ctx: SSL_CTX pointer. NULL at creation time, Not NULL when mg_context will be freed user_data: parameter user_data passed when starting the server. Return value: 0: civetweb will continue to create the context, just as if the callback would not be present. The value in *ssl_ctx when the function returns is ignored. 1: civetweb will copy the value from *ssl_ctx to the civetweb context and doesn't create its own. -1: initializing ssl fails.*/ int (*external_ssl_ctx)(void **ssl_ctx, void *user_data); /* Called when civetweb is about to create or free a SSL_CTX for a domain. Parameters: server_domain: authentication_domain from the domain config. ssl_ctx: SSL_CTX pointer. NULL at creation time, Not NULL when mg_context will be freed user_data: parameter user_data passed when starting the server. Return value: 0: civetweb will continue to create the context, just as if the callback would not be present. The value in *ssl_ctx when the function returns is ignored. 1: civetweb will copy the value from *ssl_ctx to the civetweb context and doesn't create its own. -1: initializing ssl fails.*/ int (*external_ssl_ctx_domain)(const char *server_domain, void **ssl_ctx, void *user_data); #if defined(MG_EXPERIMENTAL_INTERFACES) /* 2019-11-03 */ /* Called when data frame has been received from the peer. Parameters: bits: first byte of the websocket frame, see websocket RFC at http://tools.ietf.org/html/rfc6455, section 5.2 data, data_len: payload, with mask (if any) already applied. Return value: 1: keep this websocket connection open. 0: close this websocket connection. This callback is deprecated: Use mg_set_websocket_handler instead. */ int (*websocket_data)(struct mg_connection *, int bits, char *data, size_t data_len); #endif /* MG_LEGACY_INTERFACE */ /* Called when civetweb is closing a connection. The per-context mutex is locked when this is invoked. Websockets: Before mg_set_websocket_handler has been added, it was primarily useful for noting when a websocket is closing, and used to remove it from any application-maintained list of clients. Using this callback for websocket connections is deprecated: Use mg_set_websocket_handler instead. */ void (*connection_close)(const struct mg_connection *); /* Called after civetweb has closed a connection. The per-context mutex is locked when this is invoked. Connection specific data: If memory has been allocated for the connection specific user data (mg_request_info->conn_data, mg_get_user_connection_data), this is the last chance to free it. */ void (*connection_closed)(const struct mg_connection *); /* init_lua is called when civetweb is about to serve Lua server page. exit_lua is called when the Lua processing is complete. Both will work only if Lua support is enabled. Parameters: conn: current connection. lua_context: "lua_State *" pointer. context_flags: context type information as bitmask: context_flags & 0x0F: (0-15) Lua environment type */ void (*init_lua)(const struct mg_connection *conn, void *lua_context, unsigned context_flags); void (*exit_lua)(const struct mg_connection *conn, void *lua_context, unsigned context_flags); /* Called when civetweb is about to send HTTP error to the client. Implementing this callback allows to create custom error pages. Parameters: conn: current connection. status: HTTP error status code. errmsg: error message text. Return value: 1: run civetweb error handler. 0: callback already handled the error. */ int (*http_error)(struct mg_connection *conn, int status, const char *errmsg); /* Called after civetweb context has been created, before requests are processed. Parameters: ctx: context handle */ void (*init_context)(const struct mg_context *ctx); /* Called when civetweb context is deleted. Parameters: ctx: context handle */ void (*exit_context)(const struct mg_context *ctx); /* Called when a new worker thread is initialized. * It is always called from the newly created thread and can be used to * initialize thread local storage data. * Parameters: * ctx: context handle * thread_type: * 0 indicates the master thread * 1 indicates a worker thread handling client connections * 2 indicates an internal helper thread (timer thread) * Return value: * This function returns a user supplied pointer. The pointer is assigned * to the thread and can be obtained from the mg_connection object using * mg_get_thread_pointer in all server callbacks. Note: A connection and * a thread are not directly related. Threads will serve several different * connections, and data from a single connection may call different * callbacks using different threads. The thread pointer can be obtained * in a callback handler, but should not be stored beyond the scope of * one call to one callback. */ void *(*init_thread)(const struct mg_context *ctx, int thread_type); /* Called when a worker exits. * The parameters "ctx" and "thread_type" correspond to the "init_thread" * call. The "thread_pointer" parameter is the value returned by * "init_thread". */ void (*exit_thread)(const struct mg_context *ctx, int thread_type, void *thread_pointer); /* Called when initializing a new connection object. * Can be used to initialize the connection specific user data * (mg_request_info->conn_data, mg_get_user_connection_data). * When the callback is called, it is not yet known if a * valid HTTP(S) request will be made. * Parameters: * conn: not yet fully initialized connection object * conn_data: output parameter, set to initialize the * connection specific user data * Return value: * must be 0 * Otherwise, the result is undefined */ int (*init_connection)(const struct mg_connection *conn, void **conn_data); }; /* Start web server. Parameters: callbacks: mg_callbacks structure with user-defined callbacks. options: NULL terminated list of option_name, option_value pairs that specify Civetweb configuration parameters. Side-effects: on UNIX, ignores SIGCHLD and SIGPIPE signals. If custom processing is required for these, signal handlers must be set up after calling mg_start(). Example: const char *options[] = { "document_root", "/var/www", "listening_ports", "80,443s", NULL }; struct mg_context *ctx = mg_start(&my_func, NULL, options); Refer to https://github.com/civetweb/civetweb/blob/master/docs/UserManual.md for the list of valid option and their possible values. Return: web server context, or NULL on error. */ CIVETWEB_API struct mg_context *mg_start(const struct mg_callbacks *callbacks, void *user_data, const char **configuration_options); /* Stop the web server. Must be called last, when an application wants to stop the web server and release all associated resources. This function blocks until all Civetweb threads are stopped. Context pointer becomes invalid. */ CIVETWEB_API void mg_stop(struct mg_context *); /* Add an additional domain to an already running web server. * * Parameters: * ctx: Context handle of a server started by mg_start. * options: NULL terminated list of option_name, option_value pairs that * specify CivetWeb configuration parameters. * * Return: * < 0 in case of an error * -1 for a parameter error * -2 invalid options * -3 initializing SSL failed * -4 mandatory domain option missing * -5 duplicate domain * -6 out of memory * > 0 index / handle of a new domain */ CIVETWEB_API int mg_start_domain(struct mg_context *ctx, const char **configuration_options); /* mg_request_handler Called when a new request comes in. This callback is URI based and configured with mg_set_request_handler(). Parameters: conn: current connection information. cbdata: the callback data configured with mg_set_request_handler(). Returns: 0: the handler could not handle the request, so fall through. 1 - 999: the handler processed the request. The return code is stored as a HTTP status code for the access log. */ typedef int (*mg_request_handler)(struct mg_connection *conn, void *cbdata); /* mg_set_request_handler Sets or removes a URI mapping for a request handler. This function waits until a removing/updating handler becomes unused, so do not call from the handler itself. URI's are ordered and prefixed URI's are supported. For example, consider two URIs: /a/b and /a /a matches /a /a/b matches /a/b /a/c matches /a Parameters: ctx: server context uri: the URI (exact or pattern) for the handler handler: the callback handler to use when the URI is requested. If NULL, an already registered handler for this URI will be removed. The URI used to remove a handler must match exactly the one used to register it (not only a pattern match). cbdata: the callback data to give to the handler when it is called. */ CIVETWEB_API void mg_set_request_handler(struct mg_context *ctx, const char *uri, mg_request_handler handler, void *cbdata); /* Callback types for websocket handlers in C/C++. mg_websocket_connect_handler Is called when the client intends to establish a websocket connection, before websocket handshake. Return value: 0: civetweb proceeds with websocket handshake. 1: connection is closed immediately. mg_websocket_ready_handler Is called when websocket handshake is successfully completed, and connection is ready for data exchange. mg_websocket_data_handler Is called when a data frame has been received from the client. Parameters: bits: first byte of the websocket frame, see websocket RFC at http://tools.ietf.org/html/rfc6455, section 5.2 data, data_len: payload, with mask (if any) already applied. Return value: 1: keep this websocket connection open. 0: close this websocket connection. mg_connection_close_handler Is called, when the connection is closed.*/ typedef int (*mg_websocket_connect_handler)(const struct mg_connection *, void *); typedef void (*mg_websocket_ready_handler)(struct mg_connection *, void *); typedef int (*mg_websocket_data_handler)(struct mg_connection *, int, char *, size_t, void *); typedef void (*mg_websocket_close_handler)(const struct mg_connection *, void *); /* struct mg_websocket_subprotocols * * List of accepted subprotocols */ struct mg_websocket_subprotocols { int nb_subprotocols; const char **subprotocols; }; /* mg_set_websocket_handler Set or remove handler functions for websocket connections. This function works similar to mg_set_request_handler - see there. */ CIVETWEB_API void mg_set_websocket_handler(struct mg_context *ctx, const char *uri, mg_websocket_connect_handler connect_handler, mg_websocket_ready_handler ready_handler, mg_websocket_data_handler data_handler, mg_websocket_close_handler close_handler, void *cbdata); /* mg_set_websocket_handler Set or remove handler functions for websocket connections. This function works similar to mg_set_request_handler - see there. */ CIVETWEB_API void mg_set_websocket_handler_with_subprotocols( struct mg_context *ctx, const char *uri, struct mg_websocket_subprotocols *subprotocols, mg_websocket_connect_handler connect_handler, mg_websocket_ready_handler ready_handler, mg_websocket_data_handler data_handler, mg_websocket_close_handler close_handler, void *cbdata); /* mg_authorization_handler Callback function definition for mg_set_auth_handler Parameters: conn: current connection information. cbdata: the callback data configured with mg_set_request_handler(). Returns: 0: access denied 1: access granted */ typedef int (*mg_authorization_handler)(struct mg_connection *conn, void *cbdata); /* mg_set_auth_handler Sets or removes a URI mapping for an authorization handler. This function works similar to mg_set_request_handler - see there. */ CIVETWEB_API void mg_set_auth_handler(struct mg_context *ctx, const char *uri, mg_authorization_handler handler, void *cbdata); /* Get the value of particular configuration parameter. The value returned is read-only. Civetweb does not allow changing configuration at run time. If given parameter name is not valid, NULL is returned. For valid names, return value is guaranteed to be non-NULL. If parameter is not set, zero-length string is returned. */ CIVETWEB_API const char *mg_get_option(const struct mg_context *ctx, const char *name); /* Get context from connection. */ CIVETWEB_API struct mg_context * mg_get_context(const struct mg_connection *conn); /* Get user data passed to mg_start from context. */ CIVETWEB_API void *mg_get_user_data(const struct mg_context *ctx); /* Get user data passed to mg_start from connection. */ CIVETWEB_API void *mg_get_user_context_data(const struct mg_connection *conn); /* Get user defined thread pointer for server threads (see init_thread). */ CIVETWEB_API void *mg_get_thread_pointer(const struct mg_connection *conn); /* Set user data for the current connection. */ /* Note: CivetWeb callbacks use "struct mg_connection *conn" as input when mg_read/mg_write callbacks are allowed in the callback, while "const struct mg_connection *conn" is used as input in case calling mg_read/mg_write is not allowed. Setting the user connection data will modify the connection object represented by mg_connection *, but it will not read from or write to the connection. */ /* Note: An alternative is to use the init_connection callback instead to initialize the user connection data pointer. It is recommended to supply a pointer to some user defined data structure as conn_data initializer in init_connection. In case it is required to change some data after the init_connection call, store another data pointer in the user defined data structure and modify that pointer. In either case, after the init_connection callback, only calls to mg_get_user_connection_data should be required. */ CIVETWEB_API void mg_set_user_connection_data(const struct mg_connection *conn, void *data); /* Get user data set for the current connection. */ CIVETWEB_API void * mg_get_user_connection_data(const struct mg_connection *conn); /* Get a formatted link corresponding to the current request Parameters: conn: current connection information. buf: string buffer (out) buflen: length of the string buffer Returns: <0: error >=0: ok */ CIVETWEB_API int mg_get_request_link(const struct mg_connection *conn, char *buf, size_t buflen); struct mg_option { const char *name; int type; const char *default_value; }; /* Configuration types */ enum { MG_CONFIG_TYPE_UNKNOWN = 0x0, MG_CONFIG_TYPE_NUMBER = 0x1, MG_CONFIG_TYPE_STRING = 0x2, MG_CONFIG_TYPE_FILE = 0x3, MG_CONFIG_TYPE_DIRECTORY = 0x4, MG_CONFIG_TYPE_BOOLEAN = 0x5, MG_CONFIG_TYPE_EXT_PATTERN = 0x6, MG_CONFIG_TYPE_STRING_LIST = 0x7, MG_CONFIG_TYPE_STRING_MULTILINE = 0x8, MG_CONFIG_TYPE_YES_NO_OPTIONAL = 0x9 }; /* Return array of struct mg_option, representing all valid configuration options of civetweb.c. The array is terminated by a NULL name option. */ CIVETWEB_API const struct mg_option *mg_get_valid_options(void); struct mg_server_port { int protocol; /* 1 = IPv4, 2 = IPv6, 3 = both */ int port; /* port number */ int is_ssl; /* https port: 0 = no, 1 = yes */ int is_redirect; /* redirect all requests: 0 = no, 1 = yes */ int _reserved1; int _reserved2; int _reserved3; int _reserved4; }; /* Legacy name */ #define mg_server_ports mg_server_port /* Get the list of ports that civetweb is listening on. The parameter size is the size of the ports array in elements. The caller is responsibility to allocate the required memory. This function returns the number of struct mg_server_port elements filled in, or <0 in case of an error. */ CIVETWEB_API int mg_get_server_ports(const struct mg_context *ctx, int size, struct mg_server_port *ports); /* Add, edit or delete the entry in the passwords file. * * This function allows an application to manipulate .htpasswd files on the * fly by adding, deleting and changing user records. This is one of the * several ways of implementing authentication on the server side. For another, * cookie-based way please refer to the examples/chat in the source tree. * * Parameter: * passwords_file_name: Path and name of a file storing multiple passwords * realm: HTTP authentication realm (authentication domain) name * user: User name * password: * If password is not NULL, entry modified or added. * If password is NULL, entry is deleted. * * Return: * 1 on success, 0 on error. */ CIVETWEB_API int mg_modify_passwords_file(const char *passwords_file_name, const char *realm, const char *user, const char *password); /* Same as mg_modify_passwords_file, but instead of the plain-text * password, the HA1 hash is specified. The plain-text password is * not made known to civetweb. * * The HA1 hash is the MD5 checksum of a "user:realm:password" string * in lower-case hex format. For example, if the user name is "myuser", * the realm is "myrealm", and the password is "secret", then the HA1 is * e67fd3248b58975c3e89ff18ecb75e2f. */ CIVETWEB_API int mg_modify_passwords_file_ha1(const char *passwords_file_name, const char *realm, const char *user, const char *ha1); /* Return information associated with the request. * Use this function to implement a server and get data about a request * from a HTTP/HTTPS client. * Note: Before CivetWeb 1.10, this function could be used to read * a response from a server, when implementing a client, although the * values were never returned in appropriate mg_request_info elements. * It is strongly advised to use mg_get_response_info for clients. */ CIVETWEB_API const struct mg_request_info * mg_get_request_info(const struct mg_connection *); /* Return information associated with a HTTP/HTTPS response. * Use this function in a client, to check the response from * the server. */ CIVETWEB_API const struct mg_response_info * mg_get_response_info(const struct mg_connection *); /* Send data to the client. Return: 0 when the connection has been closed -1 on error >0 number of bytes written on success */ CIVETWEB_API int mg_write(struct mg_connection *, const void *buf, size_t len); /* Send data to a websocket client wrapped in a websocket frame. Uses mg_lock_connection to ensure that the transmission is not interrupted, i.e., when the application is proactively communicating and responding to a request simultaneously. Send data to a websocket client wrapped in a websocket frame. This function is available when civetweb is compiled with -DUSE_WEBSOCKET Return: 0 when the connection has been closed -1 on error >0 number of bytes written on success */ CIVETWEB_API int mg_websocket_write(struct mg_connection *conn, int opcode, const char *data, size_t data_len); /* Send data to a websocket server wrapped in a masked websocket frame. Uses mg_lock_connection to ensure that the transmission is not interrupted, i.e., when the application is proactively communicating and responding to a request simultaneously. Send data to a websocket server wrapped in a masked websocket frame. This function is available when civetweb is compiled with -DUSE_WEBSOCKET Return: 0 when the connection has been closed -1 on error >0 number of bytes written on success */ CIVETWEB_API int mg_websocket_client_write(struct mg_connection *conn, int opcode, const char *data, size_t data_len); /* Blocks until unique access is obtained to this connection. Intended for use with websockets only. Invoke this before mg_write or mg_printf when communicating with a websocket if your code has server-initiated communication as well as communication in direct response to a message. Do not acquire this lock while holding mg_lock_context(). */ CIVETWEB_API void mg_lock_connection(struct mg_connection *conn); CIVETWEB_API void mg_unlock_connection(struct mg_connection *conn); /* Lock server context. This lock may be used to protect resources that are shared between different connection/worker threads. If the given context is not server, these functions do nothing. */ CIVETWEB_API void mg_lock_context(struct mg_context *ctx); CIVETWEB_API void mg_unlock_context(struct mg_context *ctx); /* WebSocket OpcCodes, from http://tools.ietf.org/html/rfc6455 */ enum { MG_WEBSOCKET_OPCODE_CONTINUATION = 0x0, MG_WEBSOCKET_OPCODE_TEXT = 0x1, MG_WEBSOCKET_OPCODE_BINARY = 0x2, MG_WEBSOCKET_OPCODE_CONNECTION_CLOSE = 0x8, MG_WEBSOCKET_OPCODE_PING = 0x9, MG_WEBSOCKET_OPCODE_PONG = 0xa }; /* Macros for enabling compiler-specific checks for printf-like arguments. */ #undef PRINTF_FORMAT_STRING #if defined(_MSC_VER) && _MSC_VER >= 1400 #include #if defined(_MSC_VER) && _MSC_VER > 1400 #define PRINTF_FORMAT_STRING(s) _Printf_format_string_ s #else #define PRINTF_FORMAT_STRING(s) __format_string s #endif #else #define PRINTF_FORMAT_STRING(s) s #endif #ifdef __GNUC__ #define PRINTF_ARGS(x, y) __attribute__((format(printf, x, y))) #else #define PRINTF_ARGS(x, y) #endif /* Send data to the client using printf() semantics. Works exactly like mg_write(), but allows to do message formatting. */ CIVETWEB_API int mg_printf(struct mg_connection *, PRINTF_FORMAT_STRING(const char *fmt), ...) PRINTF_ARGS(2, 3); /* Send a part of the message body, if chunked transfer encoding is set. * Only use this function after sending a complete HTTP request or response * header with "Transfer-Encoding: chunked" set. */ CIVETWEB_API int mg_send_chunk(struct mg_connection *conn, const char *chunk, unsigned int chunk_len); /* Send contents of the entire file together with HTTP headers. * Parameters: * conn: Current connection information. * path: Full path to the file to send. * This function has been superseded by mg_send_mime_file */ CIVETWEB_API void mg_send_file(struct mg_connection *conn, const char *path); /* Send contents of the file without HTTP headers. * The code must send a valid HTTP response header before using this function. * * Parameters: * conn: Current connection information. * path: Full path to the file to send. * * Return: * < 0 Error */ CIVETWEB_API int mg_send_file_body(struct mg_connection *conn, const char *path); /* Send HTTP error reply. */ CIVETWEB_API int mg_send_http_error(struct mg_connection *conn, int status_code, PRINTF_FORMAT_STRING(const char *fmt), ...) PRINTF_ARGS(3, 4); /* Send "HTTP 200 OK" response header. * After calling this function, use mg_write or mg_send_chunk to send the * response body. * Parameters: * conn: Current connection handle. * mime_type: Set Content-Type for the following content. * content_length: Size of the following content, if content_length >= 0. * Will set transfer-encoding to chunked, if set to -1. * Return: * < 0 Error */ CIVETWEB_API int mg_send_http_ok(struct mg_connection *conn, const char *mime_type, long long content_length); /* Send "HTTP 30x" redirect response. * The response has content-size zero: do not send any body data after calling * this function. * Parameters: * conn: Current connection handle. * target_url: New location. * redirect_code: HTTP redirect type. Could be 301, 302, 303, 307, 308. * Return: * < 0 Error (-1 send error, -2 parameter error) */ CIVETWEB_API int mg_send_http_redirect(struct mg_connection *conn, const char *target_url, int redirect_code); /* Send HTTP digest access authentication request. * Browsers will send a user name and password in their next request, showing * an authentication dialog if the password is not stored. * Parameters: * conn: Current connection handle. * realm: Authentication realm. If NULL is supplied, the sever domain * set in the authentication_domain configuration is used. * Return: * < 0 Error */ CIVETWEB_API int mg_send_digest_access_authentication_request(struct mg_connection *conn, const char *realm); /* Check if the current request has a valid authentication token set. * A file is used to provide a list of valid user names, realms and * password hashes. The file can be created and modified using the * mg_modify_passwords_file API function. * Parameters: * conn: Current connection handle. * realm: Authentication realm. If NULL is supplied, the sever domain * set in the authentication_domain configuration is used. * filename: Path and name of a file storing multiple password hashes. * Return: * > 0 Valid authentication * 0 Invalid authentication * < 0 Error (all values < 0 should be considered as invalid * authentication, future error codes will have negative * numbers) * -1 Parameter error * -2 File not found */ CIVETWEB_API int mg_check_digest_access_authentication(struct mg_connection *conn, const char *realm, const char *filename); /* Send contents of the entire file together with HTTP headers. * Parameters: * conn: Current connection handle. * path: Full path to the file to send. * mime_type: Content-Type for file. NULL will cause the type to be * looked up by the file extension. */ CIVETWEB_API void mg_send_mime_file(struct mg_connection *conn, const char *path, const char *mime_type); /* Send contents of the entire file together with HTTP headers. Parameters: conn: Current connection information. path: Full path to the file to send. mime_type: Content-Type for file. NULL will cause the type to be looked up by the file extension. additional_headers: Additional custom header fields appended to the header. Each header should start with an X-, to ensure it is not included twice. NULL does not append anything. */ CIVETWEB_API void mg_send_mime_file2(struct mg_connection *conn, const char *path, const char *mime_type, const char *additional_headers); /* Store body data into a file. */ CIVETWEB_API long long mg_store_body(struct mg_connection *conn, const char *path); /* Read entire request body and store it in a file "path". Return: < 0 Error >= 0 Number of bytes stored in file "path". */ /* Read data from the remote end, return number of bytes read. Return: 0 connection has been closed by peer. No more data could be read. < 0 read error. No more data could be read from the connection. > 0 number of bytes read into the buffer. */ CIVETWEB_API int mg_read(struct mg_connection *, void *buf, size_t len); /* Get the value of particular HTTP header. This is a helper function. It traverses request_info->http_headers array, and if the header is present in the array, returns its value. If it is not present, NULL is returned. */ CIVETWEB_API const char *mg_get_header(const struct mg_connection *, const char *name); /* Get a value of particular form variable. Parameters: data: pointer to form-uri-encoded buffer. This could be either POST data, or request_info.query_string. data_len: length of the encoded data. var_name: variable name to decode from the buffer dst: destination buffer for the decoded variable dst_len: length of the destination buffer Return: On success, length of the decoded variable. On error: -1 (variable not found). -2 (destination buffer is NULL, zero length or too small to hold the decoded variable). Destination buffer is guaranteed to be '\0' - terminated if it is not NULL or zero length. */ CIVETWEB_API int mg_get_var(const char *data, size_t data_len, const char *var_name, char *dst, size_t dst_len); /* Get a value of particular form variable. Parameters: data: pointer to form-uri-encoded buffer. This could be either POST data, or request_info.query_string. data_len: length of the encoded data. var_name: variable name to decode from the buffer dst: destination buffer for the decoded variable dst_len: length of the destination buffer occurrence: which occurrence of the variable, 0 is the 1st, 1 the 2nd, ... this makes it possible to parse a query like b=x&a=y&a=z which will have occurrence values b:0, a:0 and a:1 Return: On success, length of the decoded variable. On error: -1 (variable not found). -2 (destination buffer is NULL, zero length or too small to hold the decoded variable). Destination buffer is guaranteed to be '\0' - terminated if it is not NULL or zero length. */ CIVETWEB_API int mg_get_var2(const char *data, size_t data_len, const char *var_name, char *dst, size_t dst_len, size_t occurrence); /* Split form encoded data into a list of key value pairs. A form encoded input might be a query string, the body of a x-www-form-urlencoded POST request or any other data with this structure: "keyName1=value1&keyName2=value2&keyName3=value3". Values might be percent-encoded - this function will transform them to the unencoded characters. The input string is modified by this function: To split the "query_string" member of struct request_info, create a copy first (e.g., using strdup). The function itself does not allocate memory. Thus, it is not required to free any pointer returned from this function. The output list of is limited to MG_MAX_FORM_FIELDS name-value- pairs. The default value is reasonably oversized for typical applications, however, for special purpose systems it might be required to increase this value at compile time. Parameters: data: form encoded input string. Will be modified by this function. form_fields: output list of name/value-pairs. A buffer with a size specified by num_form_fields must be provided by the caller. num_form_fields: Size of provided form_fields buffer in number of "struct mg_header" elements. Return: On success: number of form_fields filled On error: -1 (parameter error). */ CIVETWEB_API int mg_split_form_urlencoded(char *data, struct mg_header *form_fields, unsigned num_form_fields); /* Fetch value of certain cookie variable into the destination buffer. Destination buffer is guaranteed to be '\0' - terminated. In case of failure, dst[0] == '\0'. Note that RFC allows many occurrences of the same parameter. This function returns only first occurrence. Return: On success, value length. On error: -1 (either "Cookie:" header is not present at all or the requested parameter is not found). -2 (destination buffer is NULL, zero length or too small to hold the value). */ CIVETWEB_API int mg_get_cookie(const char *cookie, const char *var_name, char *buf, size_t buf_len); /* Download data from the remote web server. host: host name to connect to, e.g. "foo.com", or "10.12.40.1". port: port number, e.g. 80. use_ssl: whether to use SSL connection. error_buffer, error_buffer_size: error message placeholder. request_fmt,...: HTTP request. Return: On success, valid pointer to the new connection, suitable for mg_read(). On error, NULL. error_buffer contains error message. Example: char ebuf[100]; struct mg_connection *conn; conn = mg_download("google.com", 80, 0, ebuf, sizeof(ebuf), "%s", "GET / HTTP/1.0\r\nHost: google.com\r\n\r\n"); mg_download is equivalent to calling mg_connect_client followed by mg_printf and mg_get_response. Using these three functions directly may allow more control as compared to using mg_download. */ CIVETWEB_API struct mg_connection * mg_download(const char *host, int port, int use_ssl, char *error_buffer, size_t error_buffer_size, PRINTF_FORMAT_STRING(const char *request_fmt), ...) PRINTF_ARGS(6, 7); /* Close the connection opened by mg_download(). */ CIVETWEB_API void mg_close_connection(struct mg_connection *conn); /* This structure contains callback functions for handling form fields. It is used as an argument to mg_handle_form_request. */ struct mg_form_data_handler { /* This callback function is called, if a new field has been found. * The return value of this callback is used to define how the field * should be processed. * * Parameters: * key: Name of the field ("name" property of the HTML input field). * filename: Name of a file to upload, at the client computer. * Only set for input fields of type "file", otherwise NULL. * path: Output parameter: File name (incl. path) to store the file * at the server computer. Only used if FORM_FIELD_STORAGE_STORE * is returned by this callback. Existing files will be * overwritten. * pathlen: Length of the buffer for path. * user_data: Value of the member user_data of mg_form_data_handler * * Return value: * The callback must return the intended storage for this field * (See FORM_FIELD_STORAGE_*). */ int (*field_found)(const char *key, const char *filename, char *path, size_t pathlen, void *user_data); /* If the "field_found" callback returned FORM_FIELD_STORAGE_GET, * this callback will receive the field data. * * Parameters: * key: Name of the field ("name" property of the HTML input field). * value: Value of the input field. * user_data: Value of the member user_data of mg_form_data_handler * * Return value: * The return code determines how the server should continue processing * the current request (See MG_FORM_FIELD_HANDLE_*). */ int (*field_get)(const char *key, const char *value, size_t valuelen, void *user_data); /* If the "field_found" callback returned FORM_FIELD_STORAGE_STORE, * the data will be stored into a file. If the file has been written * successfully, this callback will be called. This callback will * not be called for only partially uploaded files. The * mg_handle_form_request function will either store the file completely * and call this callback, or it will remove any partial content and * not call this callback function. * * Parameters: * path: Path of the file stored at the server. * file_size: Size of the stored file in bytes. * user_data: Value of the member user_data of mg_form_data_handler * * Return value: * The return code determines how the server should continue processing * the current request (See MG_FORM_FIELD_HANDLE_*). */ int (*field_store)(const char *path, long long file_size, void *user_data); /* User supplied argument, passed to all callback functions. */ void *user_data; }; /* Return values definition for the "field_found" callback in * mg_form_data_handler. */ enum { /* Skip this field (neither get nor store it). Continue with the * next field. */ MG_FORM_FIELD_STORAGE_SKIP = 0x0, /* Get the field value. */ MG_FORM_FIELD_STORAGE_GET = 0x1, /* Store the field value into a file. */ MG_FORM_FIELD_STORAGE_STORE = 0x2, /* Stop parsing this request. Skip the remaining fields. */ MG_FORM_FIELD_STORAGE_ABORT = 0x10 }; /* Return values for "field_get" and "field_store" */ enum { /* Only "field_get": If there is more data in this field, get the next * chunk. Otherwise: handle the next field. */ MG_FORM_FIELD_HANDLE_GET = 0x1, /* Handle the next field */ MG_FORM_FIELD_HANDLE_NEXT = 0x8, /* Stop parsing this request */ MG_FORM_FIELD_HANDLE_ABORT = 0x10 }; /* Process form data. * Returns the number of fields handled, or < 0 in case of an error. * Note: It is possible that several fields are already handled successfully * (e.g., stored into files), before the request handling is stopped with an * error. In this case a number < 0 is returned as well. * In any case, it is the duty of the caller to remove files once they are * no longer required. */ CIVETWEB_API int mg_handle_form_request(struct mg_connection *conn, struct mg_form_data_handler *fdh); /* Convenience function -- create detached thread. Return: 0 on success, non-0 on error. */ typedef void *(*mg_thread_func_t)(void *); CIVETWEB_API int mg_start_thread(mg_thread_func_t f, void *p); /* Return builtin mime type for the given file name. For unrecognized extensions, "text/plain" is returned. */ CIVETWEB_API const char *mg_get_builtin_mime_type(const char *file_name); /* Get text representation of HTTP status code. */ CIVETWEB_API const char * mg_get_response_code_text(const struct mg_connection *conn, int response_code); /* Return CivetWeb version. */ CIVETWEB_API const char *mg_version(void); /* URL-decode input buffer into destination buffer. 0-terminate the destination buffer. form-url-encoded data differs from URI encoding in a way that it uses '+' as character for space, see RFC 1866 section 8.2.1 http://ftp.ics.uci.edu/pub/ietf/html/rfc1866.txt Return: length of the decoded data, or -1 if dst buffer is too small. */ CIVETWEB_API int mg_url_decode(const char *src, int src_len, char *dst, int dst_len, int is_form_url_encoded); /* URL-encode input buffer into destination buffer. returns the length of the resulting buffer or -1 is the buffer is too small. */ CIVETWEB_API int mg_url_encode(const char *src, char *dst, size_t dst_len); /* BASE64-encode input buffer into destination buffer. returns -1 on OK. */ CIVETWEB_API int mg_base64_encode(const unsigned char *src, size_t src_len, char *dst, size_t *dst_len); /* BASE64-decode input buffer into destination buffer. returns -1 on OK. */ CIVETWEB_API int mg_base64_decode(const char *src, size_t src_len, unsigned char *dst, size_t *dst_len); /* MD5 hash given strings. Buffer 'buf' must be 33 bytes long. Varargs is a NULL terminated list of ASCIIz strings. When function returns, buf will contain human-readable MD5 hash. Example: char buf[33]; mg_md5(buf, "aa", "bb", NULL); */ CIVETWEB_API char *mg_md5(char buf[33], ...); #if !defined(MG_MATCH_CONTEXT_MAX_MATCHES) #define MG_MATCH_CONTEXT_MAX_MATCHES (32) #endif struct mg_match_element { const char *str; /* First character matching wildcard */ size_t len; /* Number of character matching wildcard */ }; struct mg_match_context { int case_sensitive; /* Input: 1 (case sensitive) or 0 (insensitive) */ size_t num_matches; /* Output: Number of wildcard matches returned. */ struct mg_match_element match[MG_MATCH_CONTEXT_MAX_MATCHES]; /* Output */ }; #if defined(MG_EXPERIMENTAL_INTERFACES) /* Pattern matching and extraction function. Parameters: pat: Pattern string (see UserManual.md) str: String to search for match patterns. mcx: Match context (optional, can be NULL). Return: Number of characters matched. -1 if no valid match was found. Note: 0 characters might be a valid match for some patterns. */ CIVETWEB_API ptrdiff_t mg_match(const char *pat, const char *str, struct mg_match_context *mcx); #endif /* Print error message to the opened error log stream. This utilizes the provided logging configuration. conn: connection (not used for sending data, but to get perameters) fmt: format string without the line return ...: variable argument list Example: mg_cry(conn,"i like %s", "logging"); */ CIVETWEB_API void mg_cry(const struct mg_connection *conn, PRINTF_FORMAT_STRING(const char *fmt), ...) PRINTF_ARGS(2, 3); /* utility methods to compare two buffers, case insensitive. */ CIVETWEB_API int mg_strcasecmp(const char *s1, const char *s2); CIVETWEB_API int mg_strncasecmp(const char *s1, const char *s2, size_t len); /* Connect to a websocket as a client Parameters: host: host to connect to, i.e. "echo.websocket.org" or "192.168.1.1" or "localhost" port: server port use_ssl: make a secure connection to server error_buffer, error_buffer_size: buffer for an error message path: server path you are trying to connect to, i.e. if connection to localhost/app, path should be "/app" origin: value of the Origin HTTP header data_func: callback that should be used when data is received from the server user_data: user supplied argument Return: On success, valid mg_connection object. On error, NULL. Se error_buffer for details. */ CIVETWEB_API struct mg_connection * mg_connect_websocket_client(const char *host, int port, int use_ssl, char *error_buffer, size_t error_buffer_size, const char *path, const char *origin, mg_websocket_data_handler data_func, mg_websocket_close_handler close_func, void *user_data); CIVETWEB_API struct mg_connection * mg_connect_websocket_client_extensions(const char *host, int port, int use_ssl, char *error_buffer, size_t error_buffer_size, const char *path, const char *origin, const char *extensions, mg_websocket_data_handler data_func, mg_websocket_close_handler close_func, void *user_data); /* Connect to a TCP server as a client (can be used to connect to a HTTP server) Parameters: host: host to connect to, i.e. "www.wikipedia.org" or "192.168.1.1" or "localhost" port: server port use_ssl: make a secure connection to server error_buffer, error_buffer_size: buffer for an error message Return: On success, valid mg_connection object. On error, NULL. Se error_buffer for details. */ CIVETWEB_API struct mg_connection *mg_connect_client(const char *host, int port, int use_ssl, char *error_buffer, size_t error_buffer_size); struct mg_client_options { const char *host; int port; const char *client_cert; const char *server_cert; const char *host_name; /* TODO: add more data */ }; CIVETWEB_API struct mg_connection * mg_connect_client_secure(const struct mg_client_options *client_options, char *error_buffer, size_t error_buffer_size); CIVETWEB_API struct mg_connection *mg_connect_websocket_client_secure( const struct mg_client_options *client_options, char *error_buffer, size_t error_buffer_size, const char *path, const char *origin, mg_websocket_data_handler data_func, mg_websocket_close_handler close_func, void *user_data); CIVETWEB_API struct mg_connection * mg_connect_websocket_client_secure_extensions( const struct mg_client_options *client_options, char *error_buffer, size_t error_buffer_size, const char *path, const char *origin, const char *extensions, mg_websocket_data_handler data_func, mg_websocket_close_handler close_func, void *user_data); #if defined(MG_LEGACY_INTERFACE) /* 2019-11-02 */ enum { TIMEOUT_INFINITE = -1 }; #endif enum { MG_TIMEOUT_INFINITE = -1 }; /* Wait for a response from the server Parameters: conn: connection ebuf, ebuf_len: error message placeholder. timeout: time to wait for a response in milliseconds (if < 0 then wait forever) Return: On success, >= 0 On error/timeout, < 0 */ CIVETWEB_API int mg_get_response(struct mg_connection *conn, char *ebuf, size_t ebuf_len, int timeout); /* mg_response_header_* functions can be used from server callbacks * to prepare HTTP server response headers. Using this function will * allow a callback to work with HTTP/1.x and HTTP/2. */ /* Initialize a new HTTP response * Parameters: * conn: Current connection handle. * status: HTTP status code (e.g., 200 for "OK"). * Return: * 0: ok * -1: parameter error * -2: invalid connection type * -3: invalid connection status * -4: network error (only if built with NO_RESPONSE_BUFFERING) */ CIVETWEB_API int mg_response_header_start(struct mg_connection *conn, int status); /* Add a new HTTP response header line * Parameters: * conn: Current connection handle. * header: Header name. * value: Header value. * value_len: Length of header value, excluding the terminating zero. * Use -1 for "strlen(value)". * Return: * 0: ok * -1: parameter error * -2: invalid connection type * -3: invalid connection status * -4: too many headers * -5: out of memory */ CIVETWEB_API int mg_response_header_add(struct mg_connection *conn, const char *header, const char *value, int value_len); /* Add a complete header string (key + value). * This function is less efficient as compared to mg_response_header_add, * and should only be used to convert complete HTTP/1.x header lines. * Parameters: * conn: Current connection handle. * http1_headers: Header line(s) in the form "name: value\r\n". * Return: * >=0: no error, number of header lines added * -1: parameter error * -2: invalid connection type * -3: invalid connection status * -4: too many headers * -5: out of memory */ CIVETWEB_API int mg_response_header_add_lines(struct mg_connection *conn, const char *http1_headers); /* Send http response * Parameters: * conn: Current connection handle. * Return: * 0: ok * -1: parameter error * -2: invalid connection type * -3: invalid connection status * -4: sending failed (network error) */ CIVETWEB_API int mg_response_header_send(struct mg_connection *conn); /* Check which features where set when the civetweb library has been compiled. The function explicitly addresses compile time defines used when building the library - it does not mean, the feature has been initialized using a mg_init_library call. mg_check_feature can be called anytime, even before mg_init_library has been called. Parameters: feature: specifies which feature should be checked The value is a bit mask. The individual bits are defined as: 1 serve files (NO_FILES not set) 2 support HTTPS (NO_SSL not set) 4 support CGI (NO_CGI not set) 8 support IPv6 (USE_IPV6 set) 16 support WebSocket (USE_WEBSOCKET set) 32 support Lua scripts and Lua server pages (USE_LUA is set) 64 support server side JavaScript (USE_DUKTAPE is set) 128 support caching (NO_CACHING not set) 256 support server statistics (USE_SERVER_STATS is set) 512 support for on the fly compression (USE_ZLIB is set) These values are defined as MG_FEATURES_* The result is undefined, if bits are set that do not represent a defined feature (currently: feature >= 1024). The result is undefined, if no bit is set (feature == 0). Return: If a feature is available, the corresponding bit is set If a feature is not available, the bit is 0 */ CIVETWEB_API unsigned mg_check_feature(unsigned feature); /* Get information on the system. Useful for support requests. Parameters: buffer: Store system information as string here. buflen: Length of buffer (including a byte required for a terminating 0). Return: Available size of system information, excluding a terminating 0. The information is complete, if the return value is smaller than buflen. The result is a JSON formatted string, the exact content may vary. Note: It is possible to determine the required buflen, by first calling this function with buffer = NULL and buflen = NULL. The required buflen is one byte more than the returned value. */ CIVETWEB_API int mg_get_system_info(char *buffer, int buflen); /* Get context information. Useful for server diagnosis. Parameters: ctx: Context handle buffer: Store context information here. buflen: Length of buffer (including a byte required for a terminating 0). Return: Available size of system information, excluding a terminating 0. The information is complete, if the return value is smaller than buflen. The result is a JSON formatted string, the exact content may vary. Note: It is possible to determine the required buflen, by first calling this function with buffer = NULL and buflen = NULL. The required buflen is one byte more than the returned value. However, since the available context information changes, you should allocate a few bytes more. */ CIVETWEB_API int mg_get_context_info(const struct mg_context *ctx, char *buffer, int buflen); /* Disable HTTP keep-alive on a per-connection basis. Reference: https://github.com/civetweb/civetweb/issues/727 Parameters: conn: Current connection handle. */ CIVETWEB_API void mg_disable_connection_keep_alive(struct mg_connection *conn); #if defined(MG_EXPERIMENTAL_INTERFACES) /* Get connection information. Useful for server diagnosis. Parameters: ctx: Context handle idx: Connection index buffer: Store context information here. buflen: Length of buffer (including a byte required for a terminating 0). Return: Available size of system information, excluding a terminating 0. The information is complete, if the return value is smaller than buflen. The result is a JSON formatted string, the exact content may vary. Note: It is possible to determine the required buflen, by first calling this function with buffer = NULL and buflen = NULL. The required buflen is one byte more than the returned value. However, since the available context information changes, you should allocate a few bytes more. */ CIVETWEB_API int mg_get_connection_info(const struct mg_context *ctx, int idx, char *buffer, int buflen); #endif /* New APIs for enhanced option and error handling. These mg_*2 API functions have the same purpose as their original versions, but provide additional options and/or provide improved error diagnostics. Note: Experimental interfaces may change */ struct mg_error_data { unsigned code; /* error code (number) */ unsigned code_sub; /* error sub code (number) */ char *text; /* buffer for error text */ size_t text_buffer_size; /* size of buffer of "text" */ }; /* Values for error "code" in mg_error_data */ enum { /* No error */ MG_ERROR_DATA_CODE_OK = 0u, /* Caller provided invalid parameter */ MG_ERROR_DATA_CODE_INVALID_PARAM = 1u, /* "configuration_option" contains invalid element */ MG_ERROR_DATA_CODE_INVALID_OPTION = 2u, /* Initializen TLS / SSL library failed */ MG_ERROR_DATA_CODE_INIT_TLS_FAILED = 3u, /* Mandatory "configuration_option" missing */ MG_ERROR_DATA_CODE_MISSING_OPTION = 4u, /* Duplicate "authentication_domain" option */ MG_ERROR_DATA_CODE_DUPLICATE_DOMAIN = 5u, /* Not enough memory */ MG_ERROR_DATA_CODE_OUT_OF_MEMORY = 6u, /* Server already stopped */ MG_ERROR_DATA_CODE_SERVER_STOPPED = 7u, /* mg_init_library must be called first */ MG_ERROR_DATA_CODE_INIT_LIBRARY_FAILED = 8u, /* Operating system function failed */ MG_ERROR_DATA_CODE_OS_ERROR = 9u, /* Failed to bind to server ports */ MG_ERROR_DATA_CODE_INIT_PORTS_FAILED = 10u, /* Failed to switch user (option "run_as_user") */ MG_ERROR_DATA_CODE_INIT_USER_FAILED = 11u, /* Access Control List error */ MG_ERROR_DATA_CODE_INIT_ACL_FAILED = 12u, /* Global password file error */ MG_ERROR_DATA_CODE_INVALID_PASS_FILE = 13u, /* Lua background script init error */ MG_ERROR_DATA_CODE_SCRIPT_ERROR = 14u, /* Client: Host not found, invalid IP to connect */ MG_ERROR_DATA_CODE_HOST_NOT_FOUND = 15u, /* Client: TCP connect timeout */ MG_ERROR_DATA_CODE_CONNECT_TIMEOUT = 16u, /* Client: TCP connect failed */ MG_ERROR_DATA_CODE_CONNECT_FAILED = 17u, /* Error using TLS client certificate */ MG_ERROR_DATA_CODE_TLS_CLIENT_CERT_ERROR = 18u, /* Error setting trusted TLS server certificate for client connection */ MG_ERROR_DATA_CODE_TLS_SERVER_CERT_ERROR = 19u, /* Error establishing TLS connection to HTTPS server */ MG_ERROR_DATA_CODE_TLS_CONNECT_ERROR = 20u }; struct mg_init_data { const struct mg_callbacks *callbacks; /* callback function pointer */ void *user_data; /* data */ const char **configuration_options; }; #if defined(MG_EXPERIMENTAL_INTERFACES) CIVETWEB_API struct mg_connection * mg_connect_client2(const char *host, const char *protocol, int port, const char *path, struct mg_init_data *init, struct mg_error_data *error); CIVETWEB_API int mg_get_response2(struct mg_connection *conn, struct mg_error_data *error, int timeout); #endif CIVETWEB_API struct mg_context *mg_start2(struct mg_init_data *init, struct mg_error_data *error); CIVETWEB_API int mg_start_domain2(struct mg_context *ctx, const char **configuration_options, struct mg_error_data *error); #ifdef __cplusplus } #endif /* __cplusplus */ #endif /* CIVETWEB_HEADER_INCLUDED */ webfakes/src/http2.h0000644000176200001440000020000114740430263014036 0ustar liggesusers/* Prototype implementation for HTTP2. Do not use in production. * There may be memory leaks, security vulnerabilities, ... */ /***********************************************************************/ /*** HPACK ***/ /***********************************************************************/ /* hpack predefined table. See: * https://tools.ietf.org/html/rfc7541#appendix-A */ static struct mg_header hpack_predefined[62] = {{NULL, NULL}, {":authority", NULL}, {":method", "GET"}, {":method", "POST"}, {":path", "/"}, {":path", "/index.html"}, {":scheme", "http"}, {":scheme", "https"}, {":status", "200"}, {":status", "204"}, {":status", "206"}, {":status", "304"}, {":status", "400"}, {":status", "404"}, {":status", "500"}, {"accept-charset", NULL}, {"accept-encoding", NULL}, {"accept-language", NULL}, {"accept-ranges", NULL}, {"accept", NULL}, {"access-control-allow-origin", NULL}, {"age", NULL}, {"allow", NULL}, {"authorization", NULL}, {"cache-control", NULL}, {"content-encoding", NULL}, {"content-disposition", NULL}, {"content-language", NULL}, {"content-length", NULL}, {"content-location", NULL}, {"content-range", NULL}, {"content-type", NULL}, {"cookie", NULL}, {"date", NULL}, {"etag", NULL}, {"expect", NULL}, {"expires", NULL}, {"from", NULL}, {"host", NULL}, {"if-match", NULL}, {"if-modified-since", NULL}, {"if-none-match", NULL}, {"if-range", NULL}, {"if-unmodified-since", NULL}, {"last-modified", NULL}, {"link", NULL}, {"location", NULL}, {"max-forwards", NULL}, {"proxy-authenticate", NULL}, {"proxy-authorization", NULL}, {"range", NULL}, {"referer", NULL}, {"refresh", NULL}, {"retry-after", NULL}, {"server", NULL}, {"set-cookie", NULL}, {"strict-transport-security", NULL}, {"transfer-encoding", NULL}, {"user-agent", NULL}, {"vary", NULL}, {"via", NULL}, {"www-authenticate", NULL}}; /* Huffman decoding: https://tools.ietf.org/html/rfc7541#appendix-B '0' ( 48) |00000 0 [ 5] '1' ( 49) |00001 1 [ 5] '2' ( 50) |00010 2 [ 5] 'a' ( 97) |00011 3 [ 5] 'c' ( 99) |00100 4 [ 5] 'e' (101) |00101 5 [ 5] 'i' (105) |00110 6 [ 5] 'o' (111) |00111 7 [ 5] 's' (115) |01000 8 [ 5] 't' (116) |01001 9 [ 5] ' ' ( 32) |010100 14 [ 6] '%' ( 37) |010101 15 [ 6] '-' ( 45) |010110 16 [ 6] '.' ( 46) |010111 17 [ 6] '/' ( 47) |011000 18 [ 6] '3' ( 51) |011001 19 [ 6] '4' ( 52) |011010 1a [ 6] '5' ( 53) |011011 1b [ 6] '6' ( 54) |011100 1c [ 6] '7' ( 55) |011101 1d [ 6] '8' ( 56) |011110 1e [ 6] '9' ( 57) |011111 1f [ 6] '=' ( 61) |100000 20 [ 6] 'A' ( 65) |100001 21 [ 6] '_' ( 95) |100010 22 [ 6] 'b' ( 98) |100011 23 [ 6] 'd' (100) |100100 24 [ 6] 'f' (102) |100101 25 [ 6] 'g' (103) |100110 26 [ 6] 'h' (104) |100111 27 [ 6] 'l' (108) |101000 28 [ 6] 'm' (109) |101001 29 [ 6] 'n' (110) |101010 2a [ 6] 'p' (112) |101011 2b [ 6] 'r' (114) |101100 2c [ 6] 'u' (117) |101101 2d [ 6] ':' ( 58) |1011100 5c [ 7] 'B' ( 66) |1011101 5d [ 7] 'C' ( 67) |1011110 5e [ 7] 'D' ( 68) |1011111 5f [ 7] 'E' ( 69) |1100000 60 [ 7] 'F' ( 70) |1100001 61 [ 7] 'G' ( 71) |1100010 62 [ 7] 'H' ( 72) |1100011 63 [ 7] 'I' ( 73) |1100100 64 [ 7] 'J' ( 74) |1100101 65 [ 7] 'K' ( 75) |1100110 66 [ 7] 'L' ( 76) |1100111 67 [ 7] 'M' ( 77) |1101000 68 [ 7] 'N' ( 78) |1101001 69 [ 7] 'O' ( 79) |1101010 6a [ 7] 'P' ( 80) |1101011 6b [ 7] 'Q' ( 81) |1101100 6c [ 7] 'R' ( 82) |1101101 6d [ 7] 'S' ( 83) |1101110 6e [ 7] 'T' ( 84) |1101111 6f [ 7] 'U' ( 85) |1110000 70 [ 7] 'V' ( 86) |1110001 71 [ 7] 'W' ( 87) |1110010 72 [ 7] 'Y' ( 89) |1110011 73 [ 7] 'j' (106) |1110100 74 [ 7] 'k' (107) |1110101 75 [ 7] 'q' (113) |1110110 76 [ 7] 'v' (118) |1110111 77 [ 7] 'w' (119) |1111000 78 [ 7] 'x' (120) |1111001 79 [ 7] 'y' (121) |1111010 7a [ 7] 'z' (122) |1111011 7b [ 7] '&' ( 38) |11111000 f8 [ 8] '*' ( 42) |11111001 f9 [ 8] ',' ( 44) |11111010 fa [ 8] ';' ( 59) |11111011 fb [ 8] 'X' ( 88) |11111100 fc [ 8] 'Z' ( 90) |11111101 fd [ 8] '!' ( 33) |11111110|00 3f8 [10] '"' ( 34) |11111110|01 3f9 [10] '(' ( 40) |11111110|10 3fa [10] ')' ( 41) |11111110|11 3fb [10] '?' ( 63) |11111111|00 3fc [10] ''' ( 39) |11111111|010 7fa [11] '+' ( 43) |11111111|011 7fb [11] '|' (124) |11111111|100 7fc [11] '#' ( 35) |11111111|1010 ffa [12] '>' ( 62) |11111111|1011 ffb [12] ( 0) |11111111|11000 1ff8 [13] '$' ( 36) |11111111|11001 1ff9 [13] '@' ( 64) |11111111|11010 1ffa [13] '[' ( 91) |11111111|11011 1ffb [13] ']' ( 93) |11111111|11100 1ffc [13] '~' (126) |11111111|11101 1ffd [13] '^' ( 94) |11111111|111100 3ffc [14] '}' (125) |11111111|111101 3ffd [14] '<' ( 60) |11111111|1111100 7ffc [15] '`' ( 96) |11111111|1111101 7ffd [15] '{' (123) |11111111|1111110 7ffe [15] '\' ( 92) |11111111|11111110|000 7fff0 [19] (195) |11111111|11111110|001 7fff1 [19] (208) |11111111|11111110|010 7fff2 [19] (128) |11111111|11111110|0110 fffe6 [20] (130) |11111111|11111110|0111 fffe7 [20] (131) |11111111|11111110|1000 fffe8 [20] (162) |11111111|11111110|1001 fffe9 [20] (184) |11111111|11111110|1010 fffea [20] (194) |11111111|11111110|1011 fffeb [20] (224) |11111111|11111110|1100 fffec [20] (226) |11111111|11111110|1101 fffed [20] (153) |11111111|11111110|11100 1fffdc [21] (161) |11111111|11111110|11101 1fffdd [21] (167) |11111111|11111110|11110 1fffde [21] (172) |11111111|11111110|11111 1fffdf [21] (176) |11111111|11111111|00000 1fffe0 [21] (177) |11111111|11111111|00001 1fffe1 [21] (179) |11111111|11111111|00010 1fffe2 [21] (209) |11111111|11111111|00011 1fffe3 [21] (216) |11111111|11111111|00100 1fffe4 [21] (217) |11111111|11111111|00101 1fffe5 [21] (227) |11111111|11111111|00110 1fffe6 [21] (229) |11111111|11111111|00111 1fffe7 [21] (230) |11111111|11111111|01000 1fffe8 [21] (129) |11111111|11111111|010010 3fffd2 [22] (132) |11111111|11111111|010011 3fffd3 [22] (133) |11111111|11111111|010100 3fffd4 [22] (134) |11111111|11111111|010101 3fffd5 [22] (136) |11111111|11111111|010110 3fffd6 [22] (146) |11111111|11111111|010111 3fffd7 [22] (154) |11111111|11111111|011000 3fffd8 [22] (156) |11111111|11111111|011001 3fffd9 [22] (160) |11111111|11111111|011010 3fffda [22] (163) |11111111|11111111|011011 3fffdb [22] (164) |11111111|11111111|011100 3fffdc [22] (169) |11111111|11111111|011101 3fffdd [22] (170) |11111111|11111111|011110 3fffde [22] (173) |11111111|11111111|011111 3fffdf [22] (178) |11111111|11111111|100000 3fffe0 [22] (181) |11111111|11111111|100001 3fffe1 [22] (185) |11111111|11111111|100010 3fffe2 [22] (186) |11111111|11111111|100011 3fffe3 [22] (187) |11111111|11111111|100100 3fffe4 [22] (189) |11111111|11111111|100101 3fffe5 [22] (190) |11111111|11111111|100110 3fffe6 [22] (196) |11111111|11111111|100111 3fffe7 [22] (198) |11111111|11111111|101000 3fffe8 [22] (228) |11111111|11111111|101001 3fffe9 [22] (232) |11111111|11111111|101010 3fffea [22] (233) |11111111|11111111|101011 3fffeb [22] ( 1) |11111111|11111111|1011000 7fffd8 [23] (135) |11111111|11111111|1011001 7fffd9 [23] (137) |11111111|11111111|1011010 7fffda [23] (138) |11111111|11111111|1011011 7fffdb [23] (139) |11111111|11111111|1011100 7fffdc [23] (140) |11111111|11111111|1011101 7fffdd [23] (141) |11111111|11111111|1011110 7fffde [23] (143) |11111111|11111111|1011111 7fffdf [23] (147) |11111111|11111111|1100000 7fffe0 [23] (149) |11111111|11111111|1100001 7fffe1 [23] (150) |11111111|11111111|1100010 7fffe2 [23] (151) |11111111|11111111|1100011 7fffe3 [23] (152) |11111111|11111111|1100100 7fffe4 [23] (155) |11111111|11111111|1100101 7fffe5 [23] (157) |11111111|11111111|1100110 7fffe6 [23] (158) |11111111|11111111|1100111 7fffe7 [23] (165) |11111111|11111111|1101000 7fffe8 [23] (166) |11111111|11111111|1101001 7fffe9 [23] (168) |11111111|11111111|1101010 7fffea [23] (174) |11111111|11111111|1101011 7fffeb [23] (175) |11111111|11111111|1101100 7fffec [23] (180) |11111111|11111111|1101101 7fffed [23] (182) |11111111|11111111|1101110 7fffee [23] (183) |11111111|11111111|1101111 7fffef [23] (188) |11111111|11111111|1110000 7ffff0 [23] (191) |11111111|11111111|1110001 7ffff1 [23] (197) |11111111|11111111|1110010 7ffff2 [23] (231) |11111111|11111111|1110011 7ffff3 [23] (239) |11111111|11111111|1110100 7ffff4 [23] ( 9) |11111111|11111111|11101010 ffffea [24] (142) |11111111|11111111|11101011 ffffeb [24] (144) |11111111|11111111|11101100 ffffec [24] (145) |11111111|11111111|11101101 ffffed [24] (148) |11111111|11111111|11101110 ffffee [24] (159) |11111111|11111111|11101111 ffffef [24] (171) |11111111|11111111|11110000 fffff0 [24] (206) |11111111|11111111|11110001 fffff1 [24] (215) |11111111|11111111|11110010 fffff2 [24] (225) |11111111|11111111|11110011 fffff3 [24] (236) |11111111|11111111|11110100 fffff4 [24] (237) |11111111|11111111|11110101 fffff5 [24] (199) |11111111|11111111|11110110|0 1ffffec [25] (207) |11111111|11111111|11110110|1 1ffffed [25] (234) |11111111|11111111|11110111|0 1ffffee [25] (235) |11111111|11111111|11110111|1 1ffffef [25] (192) |11111111|11111111|11111000|00 3ffffe0 [26] (193) |11111111|11111111|11111000|01 3ffffe1 [26] (200) |11111111|11111111|11111000|10 3ffffe2 [26] (201) |11111111|11111111|11111000|11 3ffffe3 [26] (202) |11111111|11111111|11111001|00 3ffffe4 [26] (205) |11111111|11111111|11111001|01 3ffffe5 [26] (210) |11111111|11111111|11111001|10 3ffffe6 [26] (213) |11111111|11111111|11111001|11 3ffffe7 [26] (218) |11111111|11111111|11111010|00 3ffffe8 [26] (219) |11111111|11111111|11111010|01 3ffffe9 [26] (238) |11111111|11111111|11111010|10 3ffffea [26] (240) |11111111|11111111|11111010|11 3ffffeb [26] (242) |11111111|11111111|11111011|00 3ffffec [26] (243) |11111111|11111111|11111011|01 3ffffed [26] (255) |11111111|11111111|11111011|10 3ffffee [26] (203) |11111111|11111111|11111011|110 7ffffde [27] (204) |11111111|11111111|11111011|111 7ffffdf [27] (211) |11111111|11111111|11111100|000 7ffffe0 [27] (212) |11111111|11111111|11111100|001 7ffffe1 [27] (214) |11111111|11111111|11111100|010 7ffffe2 [27] (221) |11111111|11111111|11111100|011 7ffffe3 [27] (222) |11111111|11111111|11111100|100 7ffffe4 [27] (223) |11111111|11111111|11111100|101 7ffffe5 [27] (241) |11111111|11111111|11111100|110 7ffffe6 [27] (244) |11111111|11111111|11111100|111 7ffffe7 [27] (245) |11111111|11111111|11111101|000 7ffffe8 [27] (246) |11111111|11111111|11111101|001 7ffffe9 [27] (247) |11111111|11111111|11111101|010 7ffffea [27] (248) |11111111|11111111|11111101|011 7ffffeb [27] (250) |11111111|11111111|11111101|100 7ffffec [27] (251) |11111111|11111111|11111101|101 7ffffed [27] (252) |11111111|11111111|11111101|110 7ffffee [27] (253) |11111111|11111111|11111101|111 7ffffef [27] (254) |11111111|11111111|11111110|000 7fffff0 [27] ( 2) |11111111|11111111|11111110|0010 fffffe2 [28] ( 3) |11111111|11111111|11111110|0011 fffffe3 [28] ( 4) |11111111|11111111|11111110|0100 fffffe4 [28] ( 5) |11111111|11111111|11111110|0101 fffffe5 [28] ( 6) |11111111|11111111|11111110|0110 fffffe6 [28] ( 7) |11111111|11111111|11111110|0111 fffffe7 [28] ( 8) |11111111|11111111|11111110|1000 fffffe8 [28] ( 11) |11111111|11111111|11111110|1001 fffffe9 [28] ( 12) |11111111|11111111|11111110|1010 fffffea [28] ( 14) |11111111|11111111|11111110|1011 fffffeb [28] ( 15) |11111111|11111111|11111110|1100 fffffec [28] ( 16) |11111111|11111111|11111110|1101 fffffed [28] ( 17) |11111111|11111111|11111110|1110 fffffee [28] ( 18) |11111111|11111111|11111110|1111 fffffef [28] ( 19) |11111111|11111111|11111111|0000 ffffff0 [28] ( 20) |11111111|11111111|11111111|0001 ffffff1 [28] ( 21) |11111111|11111111|11111111|0010 ffffff2 [28] ( 23) |11111111|11111111|11111111|0011 ffffff3 [28] ( 24) |11111111|11111111|11111111|0100 ffffff4 [28] ( 25) |11111111|11111111|11111111|0101 ffffff5 [28] ( 26) |11111111|11111111|11111111|0110 ffffff6 [28] ( 27) |11111111|11111111|11111111|0111 ffffff7 [28] ( 28) |11111111|11111111|11111111|1000 ffffff8 [28] ( 29) |11111111|11111111|11111111|1001 ffffff9 [28] ( 30) |11111111|11111111|11111111|1010 ffffffa [28] ( 31) |11111111|11111111|11111111|1011 ffffffb [28] (127) |11111111|11111111|11111111|1100 ffffffc [28] (220) |11111111|11111111|11111111|1101 ffffffd [28] (249) |11111111|11111111|11111111|1110 ffffffe [28] ( 10) |11111111|11111111|11111111|111100 3ffffffc [30] ( 13) |11111111|11111111|11111111|111101 3ffffffd [30] ( 22) |11111111|11111111|11111111|111110 3ffffffe [30] (256) |11111111|11111111|11111111|111111 3fffffff [30] */ struct { uint8_t decoded; uint8_t bitcount; uint32_t encoded; } hpack_huff_dec[] = { {48, 5, 0x0}, {49, 5, 0x1}, {50, 5, 0x2}, {97, 5, 0x3}, {99, 5, 0x4}, {101, 5, 0x5}, {105, 5, 0x6}, {111, 5, 0x7}, {115, 5, 0x8}, {116, 5, 0x9}, {32, 6, 0x14}, {37, 6, 0x15}, {45, 6, 0x16}, {46, 6, 0x17}, {47, 6, 0x18}, {51, 6, 0x19}, {52, 6, 0x1a}, {53, 6, 0x1b}, {54, 6, 0x1c}, {55, 6, 0x1d}, {56, 6, 0x1e}, {57, 6, 0x1f}, {61, 6, 0x20}, {65, 6, 0x21}, {95, 6, 0x22}, {98, 6, 0x23}, {100, 6, 0x24}, {102, 6, 0x25}, {103, 6, 0x26}, {104, 6, 0x27}, {108, 6, 0x28}, {109, 6, 0x29}, {110, 6, 0x2a}, {112, 6, 0x2b}, {114, 6, 0x2c}, {117, 6, 0x2d}, {58, 7, 0x5c}, {66, 7, 0x5d}, {67, 7, 0x5e}, {68, 7, 0x5f}, {69, 7, 0x60}, {70, 7, 0x61}, {71, 7, 0x62}, {72, 7, 0x63}, {73, 7, 0x64}, {74, 7, 0x65}, {75, 7, 0x66}, {76, 7, 0x67}, {77, 7, 0x68}, {78, 7, 0x69}, {79, 7, 0x6a}, {80, 7, 0x6b}, {81, 7, 0x6c}, {82, 7, 0x6d}, {83, 7, 0x6e}, {84, 7, 0x6f}, {85, 7, 0x70}, {86, 7, 0x71}, {87, 7, 0x72}, {89, 7, 0x73}, {106, 7, 0x74}, {107, 7, 0x75}, {113, 7, 0x76}, {118, 7, 0x77}, {119, 7, 0x78}, {120, 7, 0x79}, {121, 7, 0x7a}, {122, 7, 0x7b}, {38, 8, 0xf8}, {42, 8, 0xf9}, {44, 8, 0xfa}, {59, 8, 0xfb}, {88, 8, 0xfc}, {90, 8, 0xfd}, {33, 10, 0x3f8}, {34, 10, 0x3f9}, {40, 10, 0x3fa}, {41, 10, 0x3fb}, {63, 10, 0x3fc}, {39, 11, 0x7fa}, {43, 11, 0x7fb}, {124, 11, 0x7fc}, {35, 12, 0xffa}, {62, 12, 0xffb}, {0, 13, 0x1ff8}, {36, 13, 0x1ff9}, {64, 13, 0x1ffa}, {91, 13, 0x1ffb}, {93, 13, 0x1ffc}, {126, 13, 0x1ffd}, {94, 14, 0x3ffc}, {125, 14, 0x3ffd}, {60, 15, 0x7ffc}, {96, 15, 0x7ffd}, {123, 15, 0x7ffe}, {92, 19, 0x7fff0}, {195, 19, 0x7fff1}, {208, 19, 0x7fff2}, {128, 20, 0xfffe6}, {130, 20, 0xfffe7}, {131, 20, 0xfffe8}, {162, 20, 0xfffe9}, {184, 20, 0xfffea}, {194, 20, 0xfffeb}, {224, 20, 0xfffec}, {226, 20, 0xfffed}, {153, 21, 0x1fffdc}, {161, 21, 0x1fffdd}, {167, 21, 0x1fffde}, {172, 21, 0x1fffdf}, {176, 21, 0x1fffe0}, {177, 21, 0x1fffe1}, {179, 21, 0x1fffe2}, {209, 21, 0x1fffe3}, {216, 21, 0x1fffe4}, {217, 21, 0x1fffe5}, {227, 21, 0x1fffe6}, {229, 21, 0x1fffe7}, {230, 21, 0x1fffe8}, {129, 22, 0x3fffd2}, {132, 22, 0x3fffd3}, {133, 22, 0x3fffd4}, {134, 22, 0x3fffd5}, {136, 22, 0x3fffd6}, {146, 22, 0x3fffd7}, {154, 22, 0x3fffd8}, {156, 22, 0x3fffd9}, {160, 22, 0x3fffda}, {163, 22, 0x3fffdb}, {164, 22, 0x3fffdc}, {169, 22, 0x3fffdd}, {170, 22, 0x3fffde}, {173, 22, 0x3fffdf}, {178, 22, 0x3fffe0}, {181, 22, 0x3fffe1}, {185, 22, 0x3fffe2}, {186, 22, 0x3fffe3}, {187, 22, 0x3fffe4}, {189, 22, 0x3fffe5}, {190, 22, 0x3fffe6}, {196, 22, 0x3fffe7}, {198, 22, 0x3fffe8}, {228, 22, 0x3fffe9}, {232, 22, 0x3fffea}, {233, 22, 0x3fffeb}, {1, 23, 0x7fffd8}, {135, 23, 0x7fffd9}, {137, 23, 0x7fffda}, {138, 23, 0x7fffdb}, {139, 23, 0x7fffdc}, {140, 23, 0x7fffdd}, {141, 23, 0x7fffde}, {143, 23, 0x7fffdf}, {147, 23, 0x7fffe0}, {149, 23, 0x7fffe1}, {150, 23, 0x7fffe2}, {151, 23, 0x7fffe3}, {152, 23, 0x7fffe4}, {155, 23, 0x7fffe5}, {157, 23, 0x7fffe6}, {158, 23, 0x7fffe7}, {165, 23, 0x7fffe8}, {166, 23, 0x7fffe9}, {168, 23, 0x7fffea}, {174, 23, 0x7fffeb}, {175, 23, 0x7fffec}, {180, 23, 0x7fffed}, {182, 23, 0x7fffee}, {183, 23, 0x7fffef}, {188, 23, 0x7ffff0}, {191, 23, 0x7ffff1}, {197, 23, 0x7ffff2}, {231, 23, 0x7ffff3}, {239, 23, 0x7ffff4}, {9, 24, 0xffffea}, {142, 24, 0xffffeb}, {144, 24, 0xffffec}, {145, 24, 0xffffed}, {148, 24, 0xffffee}, {159, 24, 0xffffef}, {171, 24, 0xfffff0}, {206, 24, 0xfffff1}, {215, 24, 0xfffff2}, {225, 24, 0xfffff3}, {236, 24, 0xfffff4}, {237, 24, 0xfffff5}, {199, 25, 0x1ffffec}, {207, 25, 0x1ffffed}, {234, 25, 0x1ffffee}, {235, 25, 0x1ffffef}, {192, 26, 0x3ffffe0}, {193, 26, 0x3ffffe1}, {200, 26, 0x3ffffe2}, {201, 26, 0x3ffffe3}, {202, 26, 0x3ffffe4}, {205, 26, 0x3ffffe5}, {210, 26, 0x3ffffe6}, {213, 26, 0x3ffffe7}, {218, 26, 0x3ffffe8}, {219, 26, 0x3ffffe9}, {238, 26, 0x3ffffea}, {240, 26, 0x3ffffeb}, {242, 26, 0x3ffffec}, {243, 26, 0x3ffffed}, {255, 26, 0x3ffffee}, {203, 27, 0x7ffffde}, {204, 27, 0x7ffffdf}, {211, 27, 0x7ffffe0}, {212, 27, 0x7ffffe1}, {214, 27, 0x7ffffe2}, {221, 27, 0x7ffffe3}, {222, 27, 0x7ffffe4}, {223, 27, 0x7ffffe5}, {241, 27, 0x7ffffe6}, {244, 27, 0x7ffffe7}, {245, 27, 0x7ffffe8}, {246, 27, 0x7ffffe9}, {247, 27, 0x7ffffea}, {248, 27, 0x7ffffeb}, {250, 27, 0x7ffffec}, {251, 27, 0x7ffffed}, {252, 27, 0x7ffffee}, {253, 27, 0x7ffffef}, {254, 27, 0x7fffff0}, {2, 28, 0xfffffe2}, {3, 28, 0xfffffe3}, {4, 28, 0xfffffe4}, {5, 28, 0xfffffe5}, {6, 28, 0xfffffe6}, {7, 28, 0xfffffe7}, {8, 28, 0xfffffe8}, {11, 28, 0xfffffe9}, {12, 28, 0xfffffea}, {14, 28, 0xfffffeb}, {15, 28, 0xfffffec}, {16, 28, 0xfffffed}, {17, 28, 0xfffffee}, {18, 28, 0xfffffef}, {19, 28, 0xffffff0}, {20, 28, 0xffffff1}, {21, 28, 0xffffff2}, {23, 28, 0xffffff3}, {24, 28, 0xffffff4}, {25, 28, 0xffffff5}, {26, 28, 0xffffff6}, {27, 28, 0xffffff7}, {28, 28, 0xffffff8}, {29, 28, 0xffffff9}, {30, 28, 0xffffffa}, {31, 28, 0xffffffb}, {127, 28, 0xffffffc}, {220, 28, 0xffffffd}, {249, 28, 0xffffffe}, {10, 30, 0x3ffffffc}, {13, 30, 0x3ffffffd}, {22, 30, 0x3ffffffe}, {(uint8_t)256, 30, 0x3fffffff} /* filling/termination */ }; /* highest value with 5, 6, 7, ... 28, 29, 30 and all (32) bits */ uint32_t hpack_huff_end_code[] = {0x9, 0x2d, 0x7b, 0xfd, 0, 0x3fc, 0x7fc, 0xffb, 0x1ffd, 0x3ffd, 0x7ffe, 0, 0, 0, 0x7fff2, 0xfffed, 0x1fffe8, 0x3fffeb, 0x7ffff4, 0xfffff5, 0x1ffffef, 0x3ffffee, 0x7fffff0, 0xffffffe, 0, 0x3ffffffe, 0xFFFFFFFFu}; /* lowest index with 5, 6, 7, ... 28, 29, 30 and all (32) bits */ uint8_t hpack_huff_start_index[] = {0, 10, 36, 68, 0, 74, 79, 82, 84, 90, 92, 0, 0, 0, 95, 98, 106, 119, 145, 174, 186, 190, 205, 224, 0, 253, 0}; /* Function to decode an integer from a HPACK encoded block */ /* Integers have a variable size encoding, according to the RFC. * The integer starts at index *i, idx_mask masks the available bits in * the first byte. The index *i is advanced until the end of the * encoded integer. */ static uint64_t hpack_getnum(const uint8_t *buf, int *i, uint8_t idx_mask, struct mg_context *ctx) { uint64_t num = (buf[*i] & idx_mask); (void)ctx; if (num == idx_mask) { /* Algorithm from https://tools.ietf.org/html/rfc7541#section-5.1 */ uint32_t M = 0; do { (*i)++; num = num + ((buf[*i] & 0x7F) << M); M += 7; } while ((buf[*i] & 0x80) == 0x80); } (*i)++; return num; } /* Function to decode a string from a HPACK encoded block */ /* Strings have a variable size and can be either encoded directly (8 bits * per char), or using huffman encoding (variable bits per char). * The string starts at index *i. This index is advanced until the end of * the encoded string. */ static char * hpack_decode(const uint8_t *buf, int *i, int max_i, struct mg_context *ctx) { uint64_t byte_len64; int byte_len; int bit_len; uint8_t is_huff = ((buf[*i] & 0x80) == 0x80); /* Get length of string in bytes */ byte_len64 = hpack_getnum(buf, i, 0x7f, ctx); if (byte_len64 > 1024) { /* TODO */ return NULL; } byte_len = (int)byte_len64; bit_len = byte_len * 8; /* check size */ if ((*i) + byte_len > max_i) { return NULL; } /* Now read the string */ if (!is_huff) { /* Not huffman encoded: Copy directly */ char *result = (char *)mg_malloc_ctx(byte_len + 1, ctx); if (result) { memcpy(result, buf + (*i), byte_len); result[byte_len] = 0; } (*i) += byte_len; return result; } else { /* Huffman encoded: need to decode bitwise */ const uint8_t *pData = buf + (*i); /* begin pointer of bit input string */ int bitRead = 0; /* number of encoded bits read */ uint32_t bytesStored = 0; /* number of decoded bytes stored */ uint8_t str[2048]; /* storage buffer for decoded string */ for (;;) { uint32_t accu = 0; /* accu register: collect bits */ uint8_t bc = 0; /* number of bits collected */ int n; /* Collect bits in this loop, until we have a valid huff code in * accu */ do { accu <<= 1; accu |= (pData[bitRead / 8] >> (7 - (bitRead & 7))) & 1; bitRead++; bc++; if (bitRead > bit_len) { /* We used all bits. Return the decoded string. */ str[bytesStored] = 0; /* Terminate string */ (*i) += byte_len; /* Advance parsing index */ return mg_strdup_ctx((char *)str, ctx); /* Return a string copy */ } } while ((bc < 5) || (accu > hpack_huff_end_code[bc - 5])); /* Find matching code in huffman encoding table */ for (n = hpack_huff_start_index[bc - 5]; n < 256; n++) { if (accu == hpack_huff_dec[n].encoded) { str[bytesStored] = hpack_huff_dec[n].decoded; bytesStored++; break; } } if (bytesStored == sizeof(str)) { /* too long */ return 0; } } } } static void append_bits(uint8_t *target, uint32_t offset, uint32_t value, uint8_t value_bits) { uint32_t offset_bytes = offset / 8; uint32_t offset_bits = offset % 8; uint32_t remaining_bits, ac; value &= ~(0xFFFFFFFF << value_bits); remaining_bits = 8 - offset_bits; if (value_bits <= remaining_bits) { ac = value << (remaining_bits - value_bits); target[offset_bytes] |= ac; return; } ac = value >> (value_bits - remaining_bits); target[offset_bytes] |= ac; append_bits(target, offset + remaining_bits, value, value_bits - remaining_bits); } static int hpack_encode(uint8_t *store, const char *load, int lower) { uint32_t nohuff_len = strlen(load); uint32_t len_bits = 0; uint32_t len_bytes; uint32_t spare_bits; uint32_t i; memset(store, 0, nohuff_len + 1); for (i = 0; i < nohuff_len; i++) { uint8_t b = (uint8_t)((char)(lower ? tolower(load[i]) : load[i])); int idx; for (idx = 0; idx <= 255; idx++) { if (hpack_huff_dec[idx].decoded == b) { append_bits((uint8_t *)store + 1, len_bits, hpack_huff_dec[idx].encoded, hpack_huff_dec[idx].bitcount); len_bits += hpack_huff_dec[idx].bitcount; break; } } } len_bytes = (len_bits + 7) / 8; spare_bits = len_bytes * 8 - len_bits; if (spare_bits) { append_bits((uint8_t *)store + 1, len_bits, 0xFFFFFFFF, spare_bits); } if (len_bytes >= 127) { // TODO: Shift string and encode len in more bytes return 0; } *store = 0x80 + (uint8_t)len_bytes; if ((len_bytes >= nohuff_len) && (0)) { *store = (uint8_t)nohuff_len; if (lower) { for (i = 1; i <= nohuff_len; i++) { store[i] = tolower(load[i]); } } else { memcpy(store + 1, load, nohuff_len); } return nohuff_len + 1; } else { /* int i = 0; char *test = hpack_decode(store, &i, NULL); i = i; // breakpoint for debugging / testing */ } return len_bytes + 1; } /***********************************************************************/ /*** HTTP 2 ***/ /***********************************************************************/ static const char http2_pri[] = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"; static const unsigned char http2_pri_len = 24; /* = strlen(http2_pri) */ /* Read and check the HTTP/2 primer/preface: * See https://tools.ietf.org/html/rfc7540#section-3.5 */ static int is_valid_http2_primer(struct mg_connection *conn) { size_t pri_len = http2_pri_len; char buf[32]; /* Buffer must hold 24 bytes primer */ int read_pri_len = mg_read(conn, buf, pri_len); if (read_pri_len != (int)pri_len) { /* Size does not match. * This includes cases where mg_read returns error codes */ return 0; } if (0 != memcmp(buf, http2_pri, pri_len)) { /* Primer does not match */ return 0; } /* Primer does match */ return 1; } #define mg_xwrite(conn, data, len) \ push_all((conn)->phys_ctx, \ NULL, \ (conn)->client.sock, \ (conn)->ssl, \ (const char *)(data), \ (int)(len)) static void http2_settings_acknowledge(struct mg_connection *conn) { unsigned char http2_set_ackn_frame[9] = {0, 0, 0, 4, 1, 0, 0, 0, 0}; DEBUG_TRACE("%s", "Sending settings frame"); mg_xwrite(conn, http2_set_ackn_frame, 9); } struct http2_settings { uint32_t settings_header_table_size; uint32_t settings_enable_push; uint32_t settings_max_concurrent_streams; uint32_t settings_initial_window_size; uint32_t settings_max_frame_size; uint32_t settings_max_header_list_size; }; const struct http2_settings http2_default_settings = {4096, 1, UINT32_MAX, 65535, 16384, UINT32_MAX}; const struct http2_settings http2_civetweb_server_settings = {4096, 0, 100, 65535, 16384, 65535}; enum { HTTP2_ERR_NO_ERROR = 0, HTTP2_ERR_PROTOCOL_ERROR, HTTP2_ERR_INTERNAL_ERROR, HTTP2_ERR_FLOW_CONTROL_ERROR, HTTP2_ERR_SETTINGS_TIMEOUT, HTTP2_ERR_STREAM_CLOSED, HTTP2_ERR_FRAME_SIZE_ERROR, HTTP2_ERR_REFUSED_STREAM, HTTP2_ERR_CANCEL, HTTP2_ERR_COMPRESSION_ERROR, HTTP2_ERR_CONNECT_ERROR, HTTP2_ERR_ENHANCE_YOUR_CALM, HTTP2_ERR_INADEQUATE_SECURITY, HTTP2_ERR_HTTP_1_1_REQUIRED }; static void http2_send_settings(struct mg_connection *conn, const struct http2_settings *set) { uint16_t id; uint32_t data; uint8_t http2_settings_frame[9] = {0, 0, 36, 4, 0, 0, 0, 0, 0}; mg_xwrite(conn, http2_settings_frame, 9); id = htons(1); data = htonl(set->settings_header_table_size); mg_xwrite(conn, &id, 2); mg_xwrite(conn, &data, 4); id = htons(1); data = htonl(set->settings_enable_push); mg_xwrite(conn, &id, 2); mg_xwrite(conn, &data, 4); id = htons(1); data = htonl(set->settings_max_concurrent_streams); mg_xwrite(conn, &id, 2); mg_xwrite(conn, &data, 4); id = htons(1); data = htonl(set->settings_initial_window_size); mg_xwrite(conn, &id, 2); mg_xwrite(conn, &data, 4); id = htons(1); data = htonl(set->settings_max_frame_size); mg_xwrite(conn, &id, 2); mg_xwrite(conn, &data, 4); id = htons(1); data = htonl(set->settings_max_header_list_size); mg_xwrite(conn, &id, 2); mg_xwrite(conn, &data, 4); DEBUG_TRACE("%s", "HTTP2 settings sent"); } static int http2_send_response_headers(struct mg_connection *conn) { unsigned char http2_header_frame[9] = {0, 0, 0, 1, 4, 0, 0, 0, 0}; uint8_t header_bin[1024]; uint16_t header_len = 0; int has_date = 0; int has_connection_header = 0; int i, ok; if ((conn->status_code < 100) || (conn->status_code > 999)) { /* Invalid status: Set status to "Internal Server Error" */ conn->status_code = 500; } switch (conn->status_code) { case 200: header_bin[header_len++] = 0x88; break; case 204: header_bin[header_len++] = 0x89; break; case 206: header_bin[header_len++] = 0x8A; break; case 304: header_bin[header_len++] = 0x8B; break; case 400: header_bin[header_len++] = 0x8C; break; case 404: header_bin[header_len++] = 0x8D; break; case 500: header_bin[header_len++] = 0x8E; break; default: header_bin[header_len++] = 0x48; header_bin[header_len++] = 0x03; header_bin[header_len++] = 0x30 + (conn->status_code / 100); header_bin[header_len++] = 0x30 + ((conn->status_code / 10) % 10); header_bin[header_len++] = 0x30 + (conn->status_code % 10); break; } /* Add all headers */ for (i = 0; i < conn->response_info.num_headers; i++) { uint16_t predef = 0; uint16_t j; /* Filter headers not valid in HTTP/2 */ if (!mg_strcasecmp("Connection", conn->response_info.http_headers[i].name)) { has_connection_header = 1; continue; /* do not send */ } /* Check if this header is known in HPACK (static table index 15 to 61) * see https://tools.ietf.org/html/rfc7541#appendix-A */ for (j = 15; j <= 61; j++) { if (!mg_strcasecmp(hpack_predefined[j].name, conn->response_info.http_headers[i].name)) { predef = j; break; } } if (predef) { /* Predefined header found */ header_bin[header_len++] = 0x40 + predef; } else { /* Rare header, do not index */ header_bin[header_len++] = 0x10; j = hpack_encode(header_bin + header_len, conn->response_info.http_headers[i].name, 1); header_len += j; } j = hpack_encode(header_bin + header_len, conn->response_info.http_headers[i].value, 0); header_len += j; /* Mark required headers as sent */ if (!mg_strcasecmp("Date", conn->response_info.http_headers[i].name)) { has_date = 1; } } /* Add required headers, if they have not been sent yet */ if (!has_date) { /* Create header frame */ char date[64]; uint8_t date_len; time_t curtime = time(NULL); gmt_time_string(date, sizeof(date), &curtime); date_len = (uint8_t)strlen(date); header_bin[header_len++] = 0x61; /* "Date" predefined HPACK index 33 (0x21) + 0x40 */ header_bin[header_len++] = date_len; memcpy(header_bin + header_len, date, date_len); header_len += date_len; } http2_header_frame[1] = (header_len & 0xFF00) >> 8; http2_header_frame[2] = (header_len & 0xFF); http2_header_frame[5] = (conn->http2.stream_id & 0xFF000000u) >> 24; http2_header_frame[6] = (conn->http2.stream_id & 0xFF0000u) >> 16; http2_header_frame[7] = (conn->http2.stream_id & 0xFF00u) >> 8; http2_header_frame[8] = (conn->http2.stream_id & 0xFFu); /* Send header frame */ ok = 1; if (mg_xwrite(conn, http2_header_frame, 9) != 9) { ok = 0; } else if (mg_xwrite(conn, header_bin, header_len) != header_len) { ok = 0; } if (ok) { DEBUG_TRACE("HTTP2 response header sent: stream %u", conn->http2.stream_id); } else { DEBUG_TRACE("HTTP2 response header sending error: stream %u", conn->http2.stream_id); } (void)has_connection_header; /* ignore for the moment */ return ok; } static void http2_data_frame_head(struct mg_connection *conn, uint32_t frame_size, int is_final) { unsigned char http2_data_frame[9]; uint32_t stream_id = conn->http2.stream_id; http2_data_frame[0] = (frame_size & 0xFF0000) >> 16; http2_data_frame[1] = (frame_size & 0xFF00) >> 8; http2_data_frame[2] = (frame_size & 0xFF); http2_data_frame[3] = 0; /* frame type "DATA" */ http2_data_frame[4] = (is_final ? 1 : 0); http2_data_frame[5] = (stream_id & 0xFF000000u) >> 24; http2_data_frame[6] = (stream_id & 0xFF0000u) >> 16; http2_data_frame[7] = (stream_id & 0xFF00u) >> 8; http2_data_frame[8] = (stream_id & 0xFFu); DEBUG_TRACE("HTTP2 begin data frame: stream %u, frame_size %u (final: %i)", stream_id, frame_size, is_final); mg_xwrite(conn, http2_data_frame, 9); } static void http2_send_window(struct mg_connection *conn, uint32_t stream_id, uint32_t window_size) { unsigned char http2_window_frame[9] = {0, 0, 4, 8, 0, 0, 0, 0, 0}; uint32_t data = htonl(window_size); DEBUG_TRACE("HTTP2 send window_size: stream %u, error %u", stream_id, window_size); http2_window_frame[5] = (stream_id & 0xFF000000u) >> 24; http2_window_frame[6] = (stream_id & 0xFF0000u) >> 16; http2_window_frame[7] = (stream_id & 0xFF00u) >> 8; http2_window_frame[8] = (stream_id & 0xFFu); mg_xwrite(conn, http2_window_frame, 9); mg_xwrite(conn, &data, 4); } static void http2_reset_stream(struct mg_connection *conn, uint32_t stream_id, uint32_t error_id) { unsigned char http2_reset_frame[9] = {0, 0, 4, 3, 0, 0, 0, 0, 0}; uint32_t val = htonl(error_id); DEBUG_TRACE("HTTP2 send reset: stream %u, error %u", stream_id, error_id); http2_reset_frame[5] = (stream_id & 0xFF000000u) >> 24; http2_reset_frame[6] = (stream_id & 0xFF0000u) >> 16; http2_reset_frame[7] = (stream_id & 0xFF00u) >> 8; http2_reset_frame[8] = (stream_id & 0xFFu); mg_xwrite(conn, http2_reset_frame, 9); mg_xwrite(conn, &val, 4); } static void http2_must_use_http1(struct mg_connection *conn) { DEBUG_TRACE("HTTP2 not available for this URL (%s)", conn->path_info); http2_reset_stream(conn, conn->http2.stream_id, 0xd); } /* The HTTP2 implementation collects request headers as array of dynamically * allocated string values. This array must be freed once the request is * handled. * This is different to the HTTP/1.x implementation: For HTTP/1.x, the header * list is implemented as pointers into an existing buffer, so free must not * be called for HTTP/1.x. * Thus free_buffered_request_header_list is in mod_http2.inl. */ #if defined(DEBUG) static int mem_h_count = 0; static int mem_d_count = 0; #define CHECK_LEAK_HDR_ALLOC(ptr) \ DEBUG_TRACE("H NEW %p (%i): %s", ptr, ++mem_h_count, (const char *)ptr) #define CHECK_LEAK_HDR_FREE(ptr) \ DEBUG_TRACE("H DEL %p (%i): %s", ptr, --mem_h_count, (const char *)ptr) #define CHECK_LEAK_DYN_ALLOC(ptr) \ DEBUG_TRACE("D NEW %p (%i): %s", ptr, ++mem_d_count, (const char *)ptr) #define CHECK_LEAK_DYN_FREE(ptr) \ DEBUG_TRACE("D DEL %p (%i): %s", ptr, --mem_d_count, (const char *)ptr) #else #define CHECK_LEAK_HDR_ALLOC(ptr) #define CHECK_LEAK_HDR_FREE(ptr) #define CHECK_LEAK_DYN_ALLOC(ptr) #define CHECK_LEAK_DYN_FREE(ptr) #endif /* The dynamic header table may be resized on a HTTP2 client request. * A tablesize=0 will free all memory. */ static void purge_dynamic_header_table(struct mg_connection *conn, uint32_t tableSize) { DEBUG_TRACE("HTTP2 dynamic header table set to %u", tableSize); while (conn->http2.dyn_table_size > tableSize) { conn->http2.dyn_table_size--; CHECK_LEAK_DYN_FREE( conn->http2.dyn_table[conn->http2.dyn_table_size].name); CHECK_LEAK_DYN_FREE( conn->http2.dyn_table[conn->http2.dyn_table_size].value); mg_free((void *)conn->http2.dyn_table[conn->http2.dyn_table_size].name); conn->http2.dyn_table[conn->http2.dyn_table_size].name = 0; mg_free( (void *)conn->http2.dyn_table[conn->http2.dyn_table_size].value); conn->http2.dyn_table[conn->http2.dyn_table_size].value = 0; } } /* Internal function to free request header list. * Not to be confused with the response header list. */ static void free_buffered_request_header_list(struct mg_connection *conn) { while (conn->request_info.num_headers > 0) { conn->request_info.num_headers--; CHECK_LEAK_HDR_FREE( conn->request_info.http_headers[conn->request_info.num_headers] .name); CHECK_LEAK_HDR_FREE( conn->request_info.http_headers[conn->request_info.num_headers] .value); mg_free((void *)conn->request_info .http_headers[conn->request_info.num_headers] .name); conn->request_info.http_headers[conn->request_info.num_headers].name = 0; mg_free((void *)conn->request_info .http_headers[conn->request_info.num_headers] .value); conn->request_info.http_headers[conn->request_info.num_headers].value = 0; } } /* HTTP2 requires a different handling loop */ static void handle_http2(struct mg_connection *conn) { unsigned char http2_frame_head[9]; uint32_t http2_frame_size; uint8_t http2_frame_type; uint8_t http2_frame_flags; uint32_t http2_frame_stream_id; uint32_t http_window_length = 0; int bytes_read; uint8_t *buf; int my_settings_accepted = 0; const char *my_hpack_headers[128]; struct http2_settings client_settings = http2_default_settings; struct http2_settings server_settings = http2_default_settings; /* Send own settings */ http2_send_settings(conn, &http2_civetweb_server_settings); // http2_send_window(conn, 0, /* 0x3fff0001 */ 1024*1024); /* initialize hpack header table with predefined header fields */ memset((void *)my_hpack_headers, 0, sizeof(my_hpack_headers)); memcpy((void *)my_hpack_headers, hpack_predefined, sizeof(hpack_predefined)); buf = (uint8_t *)mg_malloc_ctx(server_settings.settings_max_frame_size, conn->phys_ctx); if (!buf) { /* Out of memory */ DEBUG_TRACE("%s", "Out of memory for HTTP2 frame"); return; } for (;;) { /* HTTP/2 is handled frame by frame */ int frame_is_end_stream = 0; int frame_is_end_headers = 0; int frame_is_padded = 0; int frame_is_priority = 0; #if defined(USE_SERVER_STATS) conn->conn_state = 3; /* HTTP/2 ready */ #endif bytes_read = mg_read(conn, http2_frame_head, sizeof(http2_frame_head)); if (bytes_read != sizeof(http2_frame_head)) { /* TODO: errormsg */ goto clean_http2; } /* Extract data from frame header */ http2_frame_size = ((uint32_t)http2_frame_head[0] * 0x10000u) + ((uint32_t)http2_frame_head[1] * 0x100u) + ((uint32_t)http2_frame_head[2]); http2_frame_type = http2_frame_head[3]; http2_frame_flags = http2_frame_head[4]; http2_frame_stream_id = ((uint32_t)http2_frame_head[5] * 0x1000000u) + ((uint32_t)http2_frame_head[6] * 0x10000u) + ((uint32_t)http2_frame_head[7] * 0x100u) + ((uint32_t)http2_frame_head[8]); frame_is_end_stream = (0 != (http2_frame_flags & 0x01)); frame_is_end_headers = (0 != (http2_frame_flags & 0x04)); frame_is_padded = (0 != (http2_frame_flags & 0x08)); frame_is_priority = (0 != (http2_frame_flags & 0x20)); if (http2_frame_size > server_settings.settings_max_frame_size) { /* TODO: Error Message */ DEBUG_TRACE("HTTP2 frame too large (%lu)", (unsigned long)http2_frame_size); goto clean_http2; } bytes_read = mg_read(conn, buf, http2_frame_size); if (bytes_read != (int)http2_frame_size) { /* TODO: Error Message - or read again? */ DEBUG_TRACE("HTTP2 read error (%li != %li)", (signed long int)bytes_read, (signed long int)http2_frame_size); goto clean_http2; } DEBUG_TRACE("HTTP2 frame type %u, size %u, stream %u, flags %02x", http2_frame_type, http2_frame_size, http2_frame_stream_id, http2_frame_flags); /* Further processing according to frame type. See definition: */ /* https://tools.ietf.org/html/rfc7540#section-6 */ switch (http2_frame_type) { case 0: /* DATA */ { /* TODO */ DEBUG_TRACE("%s", "HTTP2 DATA frame?"); } break; case 1: /* HEADERS */ { int i = 0; uint8_t padding = 0; uint32_t dependency = 0; uint8_t weight = 0; uint8_t exclusive = 0; /* Request start time */ clock_gettime(CLOCK_MONOTONIC, &(conn->req_time)); if (frame_is_padded) { padding = buf[i]; i++; DEBUG_TRACE("HTTP2 frame padded by %u bytes", padding); } if (frame_is_priority) { uint32_t val = ((uint32_t)buf[0 + i] * 0x1000000u) + ((uint32_t)buf[1 + i] * 0x10000u) + ((uint32_t)buf[2 + i] * 0x100u) + ((uint32_t)buf[3 + i]); dependency = (val & 0x7FFFFFFFu); exclusive = ((val & 0x80000000u) != 0); weight = buf[4 + i]; i += 5; DEBUG_TRACE( "HTTP2 frame weight %u, dependency %u (exclusive: %i)", weight, dependency, exclusive); } conn->request_info.num_headers = 0; while (i < (int)http2_frame_size - (int)padding) { const char *key = 0; const char *val = 0; uint8_t idx_mask = 0; uint8_t value_known = 0; uint8_t indexing = 0; uint64_t idx = 0; /* Classify next entry by checking the bit mask */ if ((buf[i] & 0x80u) == 0x80u) { /* Indexed Header Field Representation: * https://tools.ietf.org/html/rfc7541#section-6.1 */ idx_mask = 0x7fu; value_known = 1; } else if ((buf[i] & 0xC0u) == 0x40u) { /* Literal Header Field with Incremental Indexing: * https://tools.ietf.org/html/rfc7541#section-6.2.1 */ idx_mask = 0x3fu; indexing = 1; } else if ((buf[i] & 0xF0u) == 0x00u) { /* Literal Header Field without Indexing: * https://tools.ietf.org/html/rfc7541#section-6.2.2 */ idx_mask = 0x0fu; } else if ((buf[i] & 0xF0u) == 0x10u) { /* Literal Header Field Never Indexed: * https://tools.ietf.org/html/rfc7541#section-6.2.3 */ idx_mask = 0x0fu; } else if ((buf[i] & 0xE0u) == 0x20u) { uint64_t tableSize; /* Dynamic Table Size Update: * https://tools.ietf.org/html/rfc7541#section-6.3 */ idx_mask = 0x1fu; tableSize = hpack_getnum(buf, &i, idx_mask, conn->phys_ctx); /* TODO: check if tablesize > allowed table size */ /* Purge additional table entries */ purge_dynamic_header_table(conn, (uint32_t)tableSize); /* Process next frame */ continue; } else { DEBUG_TRACE("HTTP2 unknown start pattern %02x", buf[i]); goto clean_http2; } /* Get the header name table index */ idx = hpack_getnum(buf, &i, idx_mask, conn->phys_ctx); /* Get Header name "key" */ if (idx == 0) { /* Index 0: Header name encoded in following bytes */ key = hpack_decode(buf, &i, (int)bytes_read, conn->phys_ctx); CHECK_LEAK_HDR_ALLOC(key); if (!key) { DEBUG_TRACE("HTTP2 key decoding error"); goto clean_http2; } } else if (/*(idx >= 15) &&*/ (idx <= 61)) { /* Take key name from predefined header table */ key = mg_strdup_ctx(hpack_predefined[idx].name, conn->phys_ctx); /* leak? */ CHECK_LEAK_HDR_ALLOC(key); } else if ((idx >= 62) && ((idx - 61) <= conn->http2.dyn_table_size)) { /* Take from dynamic header table */ uint32_t local_table_idx = (uint32_t)idx - 62; key = mg_strdup_ctx( conn->http2.dyn_table[local_table_idx].name, conn->phys_ctx); CHECK_LEAK_HDR_ALLOC(key); } else { /* protocol violation */ DEBUG_TRACE("HTTP2 invalid index %lu", (unsigned long)idx); goto clean_http2; } /* key is allocated now and must be freed later */ /* Get header value */ if (value_known) { /* Server must already know the value */ if (idx <= 61) { if (hpack_predefined[idx].value) { val = mg_strdup_ctx(hpack_predefined[idx].value, conn->phys_ctx); /* leak? */ CHECK_LEAK_HDR_ALLOC(val); } else { /* protocol violation */ DEBUG_TRACE("HTTP2 indexed header %lu has no value " "(key: %s)", (unsigned long)idx, key); CHECK_LEAK_HDR_FREE(key); mg_free((void *)key); goto clean_http2; } } else if ((idx >= 62) && ((idx - 61) <= conn->http2.dyn_table_size)) { uint32_t local_table_idx = (uint32_t)idx - 62; val = mg_strdup_ctx( conn->http2.dyn_table[local_table_idx].value, conn->phys_ctx); CHECK_LEAK_HDR_ALLOC(val); } else { /* protocol violation */ DEBUG_TRACE( "HTTP2 indexed header %lu out of range (key: %s)", (unsigned long)idx, key); CHECK_LEAK_HDR_FREE(key); mg_free((void *)key); goto clean_http2; } } else { /* Read value from HTTP2 stream */ val = hpack_decode(buf, &i, (int)bytes_read, conn->phys_ctx); /* leak? */ CHECK_LEAK_HDR_ALLOC(val); if (!val) { DEBUG_TRACE("HTTP2 value decoding error"); mg_free((void *)key); goto clean_http2; } if (indexing) { /* Add to index */ if (conn->http2.dyn_table_size >= HTTP2_DYN_TABLE_SIZE) { /* Too many elements */ DEBUG_TRACE("HTTP2 index table is full (key: %s, " "value: %s)", key, val); CHECK_LEAK_HDR_FREE(key); CHECK_LEAK_HDR_FREE(val); mg_free((void *)key); mg_free((void *)val); goto clean_http2; } /* Add to table of dynamic headers */ conn->http2.dyn_table[conn->http2.dyn_table_size].name = mg_strdup_ctx(key, conn->phys_ctx); /* leak */ conn->http2.dyn_table[conn->http2.dyn_table_size] .value = mg_strdup_ctx(val, conn->phys_ctx); /* leak */ CHECK_LEAK_DYN_ALLOC( conn->http2.dyn_table[conn->http2.dyn_table_size] .name); CHECK_LEAK_DYN_ALLOC( conn->http2.dyn_table[conn->http2.dyn_table_size] .value); conn->http2.dyn_table_size++; DEBUG_TRACE("HTTP2 new dynamic header table entry %i " "(key: %s, value: %s)", (int)conn->http2.dyn_table_size, key, val); } } /* val and key are allocated now and must be freed later */ /* Store these pointers in conn->request_info[].http_headers, * free_buffered_header_list(conn) will clean up later. */ /* Add header for this request */ if ((key != NULL) && (val != NULL) && (conn->request_info.num_headers < MG_MAX_HEADERS)) { conn->request_info .http_headers[conn->request_info.num_headers] .name = key; conn->request_info .http_headers[conn->request_info.num_headers] .value = val; conn->request_info.num_headers++; /* Some headers need to be stored in the request structure */ if (!strcmp(":method", key)) { conn->request_info.request_method = val; } else if (!strcmp(":path", key)) { conn->request_info.local_uri = val; conn->request_info.request_uri = val; conn->request_info.local_uri_raw = val; } else if (!strcmp(":status", key)) { conn->status_code = atoi(val); } DEBUG_TRACE("HTTP2 request header (key: %s, value: %s)", key, val); } else { /* - either key or value are NULL (out of memory) * - or the max. number of headers is reached * in both cases free all memory */ DEBUG_TRACE("%s", "HTTP2 cannot add header"); CHECK_LEAK_HDR_FREE(key); CHECK_LEAK_HDR_FREE(val); mg_free((void *)key); key = NULL; mg_free((void *)val); val = NULL; } } /* stream id */ conn->http2.stream_id = http2_frame_stream_id; /* header parsed */ DEBUG_TRACE("HTTP2 handle_request (stream %u)", http2_frame_stream_id); handle_request_stat_log(conn); /* Send "final" frame */ DEBUG_TRACE("HTTP2 handle_request done (stream %u)", http2_frame_stream_id); http2_data_frame_head(conn, 0, 1); free_buffered_response_header_list(conn); free_buffered_request_header_list(conn); } break; case 2: /* PRIORITY */ { uint32_t dependStream = ((uint32_t)buf[0] * 0x1000000u) + ((uint32_t)buf[1] * 0x10000u) + ((uint32_t)buf[2] * 0x100u) + ((uint32_t)buf[3]); uint8_t weight = buf[4]; DEBUG_TRACE("HTTP2 priority %u dependent stream %u", weight, dependStream); } break; case 3: /* RST_STREAM */ { uint32_t errorId = ((uint32_t)buf[0] * 0x1000000u) + ((uint32_t)buf[1] * 0x10000u) + ((uint32_t)buf[2] * 0x100u) + ((uint32_t)buf[3]); DEBUG_TRACE("HTTP2 reset with error %u", errorId); } break; case 4: /* SETTINGS */ if (http2_frame_stream_id != 0) { /* Send protocol error */ http2_reset_stream(conn, http2_frame_stream_id, HTTP2_ERR_PROTOCOL_ERROR); DEBUG_TRACE("%s", "HTTP2 received invalid settings frame"); } else if (http2_frame_flags) { /* ACK frame. Do not reply. */ my_settings_accepted++; DEBUG_TRACE("%s", "CivetWeb settings confirmed by peer"); } else { int i; for (i = 0; i < (int)http2_frame_size; i += 6) { uint16_t id = ((uint16_t)buf[i] * 0x100u) + ((uint16_t)buf[i + 1]); uint32_t val = ((uint32_t)buf[i + 2] * 0x1000000u) + ((uint32_t)buf[i + 3] * 0x10000u) + ((uint32_t)buf[i + 4] * 0x100u) + ((uint32_t)buf[i + 5]); switch (id) { case 1: client_settings.settings_header_table_size = val; DEBUG_TRACE("Received settings header_table_size: %u", val); break; case 2: client_settings.settings_enable_push = (val != 0); DEBUG_TRACE("Received settings enable_push: %u", val); break; case 3: client_settings.settings_max_concurrent_streams = val; DEBUG_TRACE( "Received settings max_concurrent_streams: %u", val); break; case 4: client_settings.settings_initial_window_size = val; DEBUG_TRACE("Received settings initial_window_size: %u", val); break; case 5: client_settings.settings_max_frame_size = val; DEBUG_TRACE("Received settings max_frame_size: %u", val); break; case 6: client_settings.settings_max_header_list_size = val; DEBUG_TRACE( "Received settings max_header_list_size: %u", val); break; default: /* Unknown setting. Ignore it. */ DEBUG_TRACE("Received unknown settings id=%u: %u", id, val); break; } } /* Every settings frame must be acknowledged */ http2_settings_acknowledge(conn); } break; case 5: /* PUSH_PROMISE */ DEBUG_TRACE("%s", "Push promise not supported"); break; case 6: /* PING */ if (http2_frame_flags == 0) { /* Set "reply" flag, and send same data back */ DEBUG_TRACE("%s", "Replying to ping"); http2_frame_head[4] = 1; mg_xwrite(conn, http2_frame_head, sizeof(http2_frame_head)); mg_xwrite(conn, buf, http2_frame_size); } break; case 7: /* GOAWAY */ { uint32_t lastStream = ((uint32_t)buf[0] * 0x1000000u) + ((uint32_t)buf[1] * 0x10000u) + ((uint32_t)buf[2] * 0x100u) + ((uint32_t)buf[3]); uint32_t errorId = ((uint32_t)buf[4] * 0x1000000u) + ((uint32_t)buf[5] * 0x10000u) + ((uint32_t)buf[6] * 0x100u) + ((uint32_t)buf[7]); ; /* followed by debug data */ uint32_t debugDataLen = http2_frame_size - 8; char *debugData = (char *)buf + 8; DEBUG_TRACE("HTTP2 goaway stream %u, error %u (%.*s)", lastStream, errorId, debugDataLen, debugData); } break; case 8: /* WINDOW_UPDATE */ { uint32_t val = ((uint32_t)buf[0] * 0x1000000u) + ((uint32_t)buf[1] * 0x10000u) + ((uint32_t)buf[2] * 0x100u) + ((uint32_t)buf[3]); http_window_length = (val & 0x7FFFFFFFu); DEBUG_TRACE("HTTP2 window update stream %u, length %u", http2_frame_stream_id, http_window_length); } break; case 9: /* CONTINUATION */ DEBUG_TRACE("%s", "HTTP2 Continue"); break; default: /* TODO: Error Message */ DEBUG_TRACE("%s", "Unknown frame type"); goto clean_http2; } /* not used in the moment */ (void)frame_is_end_stream; (void)frame_is_end_headers; (void)client_settings; } clean_http2: DEBUG_TRACE("%s", "HTTP2 free buffer, connection handler finished"); mg_free(buf); } #if 0 static void HPACK_TEST() { uint64_t test; for (test = 0;; test++) { char in[32] = {0}; uint8_t out[32] = {0}; char *check; int i; int l; memcpy(in, &test, sizeof(test)); l = hpack_encode(out, in, 0); i = 0; check = hpack_decode(out, &i, NULL); if (strcmp(in, check)) { printf("Error\n"); } mg_free(check); } } static void HPACK_TABLE_TEST() { int i; uint32_t hpack_huff_end_code_expected[32] = { 0 }; uint8_t hpack_huff_start_index_expected[32] = { 0 }; int reverse_map[256] = { 0 }; for (i = 0; i < 256; i++) { reverse_map[i] = -1; } for (i = 0; i < 256; i++) { uint8_t bits = hpack_huff_dec[i].bitcount; uint8_t dec = hpack_huff_dec[i].decoded; if (bits > hpack_huff_dec[i + 1].bitcount) { ck_abort_msg("hpack_huff_dec disorder at index %i", i); } if (hpack_huff_dec[i].encoded & (0xFFFFFFFFul << bits)) { ck_abort_msg("hpack_huff_dec bits inconsistent at index %i", i); } if ((bits < 5) || (bits > 30)) { ck_abort_msg("hpack_huff_dec bits out of range at index %i", i); } if (reverse_map[dec] != -1) { ck_abort_msg("hpack_huff_dec duplicate: %i", hpack_huff_dec[i].decoded); } reverse_map[dec] = i; hpack_huff_end_code_expected[bits - 5] = hpack_huff_dec[i].encoded; } for (i = 255; i >= 0; i--) { uint8_t bits = hpack_huff_dec[i].bitcount; hpack_huff_start_index_expected[bits - 5] = i; } for (i = 0; i < 256; i++) { if (reverse_map[i] == -1) { ck_abort_msg("reverse map at %i missing", i); } } i = sizeof(hpack_huff_start_index) / sizeof(hpack_huff_start_index[0]); if (i != 27) { ck_abort_msg("hpack_huff_start_index size error: ", i); } i = sizeof(hpack_huff_end_code) / sizeof(hpack_huff_end_code[0]); if (i != 27) { ck_abort_msg("hpack_huff_end_code size error: ", i); } for (i = 0; i < 27; i++) { if (hpack_huff_start_index_expected[i] != hpack_huff_start_index[i]) { ck_abort_msg("hpack_huff_start_index error at %i", i); } if (hpack_huff_end_code_expected[i] != hpack_huff_end_code[i]) { ck_abort_msg("hpack_huff_end_code error at %i", i); } } } #endif static void process_new_http2_connection(struct mg_connection *conn) { if (!is_valid_http2_primer(conn)) { /* Primer does not match expectation from RFC. * See https://tools.ietf.org/html/rfc7540#section-3.5 */ DEBUG_TRACE("%s", "No valid HTTP2 primer"); mg_send_http_error(conn, 400, "%s", "Invalid HTTP/2 primer"); } else { /* Valid HTTP/2 primer received */ DEBUG_TRACE("%s", "Start handling HTTP2"); handle_http2(conn); /* Free memory allocated for headers, if not done yet */ DEBUG_TRACE("%s", "Free remaining HTTP2 header memory"); free_buffered_response_header_list(conn); free_buffered_request_header_list(conn); purge_dynamic_header_table(conn, 0); } } webfakes/src/match.h0000644000176200001440000001432714740430263014107 0ustar liggesusers/* Reimplementation of pattern matching */ /* This file is part of the CivetWeb web server. * See https://github.com/civetweb/civetweb/ */ /* Initialize structure with 0 matches */ static void match_context_reset(struct mg_match_context *mcx) { mcx->num_matches = 0; memset(mcx->match, 0, sizeof(mcx->match)); } /* Add a new match to the list of matches */ static void match_context_push(const char *str, size_t len, struct mg_match_context *mcx) { if (mcx->num_matches < MG_MATCH_CONTEXT_MAX_MATCHES) { mcx->match[mcx->num_matches].str = str; mcx->match[mcx->num_matches].len = len; mcx->num_matches++; } } static ptrdiff_t mg_match_impl(const char *pat, size_t pat_len, const char *str, struct mg_match_context *mcx) { /* Parse string */ size_t i_pat = 0; /* Pattern index */ size_t i_str = 0; /* Pattern index */ int case_sensitive = ((mcx != NULL) ? mcx->case_sensitive : 0); /* 0 or 1 */ while (i_pat < pat_len) { /* Pattern ? matches one character, except / and NULL character */ if ((pat[i_pat] == '?') && (str[i_str] != '\0') && (str[i_str] != '/')) { size_t i_str_start = i_str; do { /* Advance as long as there are ? */ i_pat++; i_str++; } while ((pat[i_pat] == '?') && (str[i_str] != '\0') && (str[i_str] != '/') && (i_pat < pat_len)); /* If we have a match context, add the substring we just found */ if (mcx) { match_context_push(str + i_str_start, i_str - i_str_start, mcx); } /* Reached end of pattern ? */ if (i_pat == pat_len) { return (ptrdiff_t)i_str; } } /* Pattern $ matches end of string */ if (pat[i_pat] == '$') { return (str[i_str] == '\0') ? (ptrdiff_t)i_str : -1; } /* Pattern * or ** matches multiple characters */ if (pat[i_pat] == '*') { size_t len; /* length matched by "*" or "**" */ ptrdiff_t ret; i_pat++; if ((pat[i_pat] == '*') && (i_pat < pat_len)) { /* Pattern ** matches all */ i_pat++; len = strlen(str + i_str); } else { /* Pattern * matches all except / character */ len = strcspn(str + i_str, "/"); } if (i_pat == pat_len) { /* End of pattern reached. Add all to match context. */ if (mcx) { match_context_push(str + i_str, len, mcx); } return ((ptrdiff_t)(i_str + len)); } /* This loop searches for the longest possible match */ do { ret = mg_match_impl(pat + i_pat, (pat_len - (size_t)i_pat), str + i_str + len, mcx); } while ((ret == -1) && (len-- > 0)); /* If we have a match context, add the substring we just found */ if (ret >= 0) { if (mcx) { match_context_push(str + i_str, len, mcx); } return ((ptrdiff_t)i_str + ret + (ptrdiff_t)len); } return -1; } /* Single character compare */ if (case_sensitive) { if (pat[i_pat] != str[i_str]) { /* case sensitive compare: mismatch */ return -1; } } else if (lowercase(&pat[i_pat]) != lowercase(&str[i_str])) { /* case insensitive compare: mismatch */ return -1; } i_pat++; i_str++; } return (ptrdiff_t)i_str; } static ptrdiff_t mg_match_alternatives(const char *pat, size_t pat_len, const char *str, struct mg_match_context *mcx) { const char *match_alternative = (const char *)memchr(pat, '|', pat_len); if (mcx != NULL) { match_context_reset(mcx); } while (match_alternative != NULL) { /* Split at | for alternative match */ size_t left_size = (size_t)(match_alternative - pat); /* Try left string first */ ptrdiff_t ret = mg_match_impl(pat, left_size, str, mcx); if (ret >= 0) { /* A 0-byte match is also valid */ return ret; } /* Reset possible incomplete match data */ if (mcx != NULL) { match_context_reset(mcx); } /* If no match: try right side */ pat += left_size + 1; pat_len -= left_size + 1; match_alternative = (const char *)memchr(pat, '|', pat_len); } /* Handled all | operators. This is the final string. */ return mg_match_impl(pat, pat_len, str, mcx); } static int match_compare(const void *p1, const void *p2, void *user) { const struct mg_match_element *e1 = (const struct mg_match_element *)p1; const struct mg_match_element *e2 = (const struct mg_match_element *)p2; /* unused */ (void)user; if (e1->str > e2->str) { return +1; } if (e1->str < e2->str) { return -1; } return 0; } #if defined(MG_EXPERIMENTAL_INTERFACES) CIVETWEB_API #else static #endif ptrdiff_t mg_match(const char *pat, const char *str, struct mg_match_context *mcx) { size_t pat_len = strlen(pat); ptrdiff_t ret = mg_match_alternatives(pat, pat_len, str, mcx); if (mcx != NULL) { if (ret < 0) { /* Remove possible incomplete data */ match_context_reset(mcx); } else { /* Join "?*" to one pattern. */ size_t i, j; /* Use difference of two array elements instead of sizeof, since * there may be some additional padding bytes. */ size_t elmsize = (size_t)(&mcx->match[1]) - (size_t)(&mcx->match[0]); /* First sort the matches by address ("str" begin to end) */ mg_sort(mcx->match, mcx->num_matches, elmsize, match_compare, NULL); /* Join consecutive matches */ i = 1; while (i < mcx->num_matches) { if ((mcx->match[i - 1].str + mcx->match[i - 1].len) == mcx->match[i].str) { /* Two matches are consecutive. Join length. */ mcx->match[i - 1].len += mcx->match[i].len; /* Shift all list elements. */ for (j = i + 1; j < mcx->num_matches; j++) { mcx->match[j - 1].len = mcx->match[j].len; mcx->match[j - 1].str = mcx->match[j].str; } /* Remove/blank last list element. */ mcx->num_matches--; mcx->match[mcx->num_matches].str = NULL; mcx->match[mcx->num_matches].len = 0; } else { i++; } } } } return ret; } static ptrdiff_t match_prefix(const char *pattern, size_t pattern_len, const char *str) { if (pattern == NULL) { return -1; } return mg_match_alternatives(pattern, pattern_len, str, NULL); } static ptrdiff_t match_prefix_strlen(const char *pattern, const char *str) { if (pattern == NULL) { return -1; } return mg_match_alternatives(pattern, strlen(pattern), str, NULL); } /* End of match.inl */ webfakes/src/mod_mbedtls.h0000644000176200001440000001564414740430263015307 0ustar liggesusers#if defined(USE_MBEDTLS) // USE_MBEDTLS used with NO_SSL #include "mbedtls/ctr_drbg.h" #include "mbedtls/debug.h" #include "mbedtls/entropy.h" #include "mbedtls/error.h" #if MBEDTLS_VERSION_NUMBER >= 0x03000000 // The file include/mbedtls/net.h was removed in v3.0.0 because its only // function was to include mbedtls/net_sockets.h which now should be included // directly. #include "mbedtls/net_sockets.h" #else #include "mbedtls/net.h" #endif #include "mbedtls/pk.h" #include "mbedtls/platform.h" #include "mbedtls/ssl.h" #include "mbedtls/x509.h" #include "mbedtls/x509_crt.h" #include typedef mbedtls_ssl_context SSL; typedef struct { mbedtls_ssl_config conf; /* SSL configuration */ mbedtls_x509_crt cert; /* Certificate */ mbedtls_ctr_drbg_context ctr; /* Counter random generator state */ mbedtls_entropy_context entropy; /* Entropy context */ mbedtls_pk_context pkey; /* Private key */ } SSL_CTX; /* public api */ int mbed_sslctx_init(SSL_CTX *ctx, const char *crt); void mbed_sslctx_uninit(SSL_CTX *ctx); void mbed_ssl_close(mbedtls_ssl_context *ssl); int mbed_ssl_accept(mbedtls_ssl_context **ssl, SSL_CTX *ssl_ctx, int *sock, struct mg_context *phys_ctx); int mbed_ssl_read(mbedtls_ssl_context *ssl, unsigned char *buf, int len); int mbed_ssl_write(mbedtls_ssl_context *ssl, const unsigned char *buf, int len); static void mbed_debug(void *context, int level, const char *file, int line, const char *str); static int mbed_ssl_handshake(mbedtls_ssl_context *ssl); int mbed_sslctx_init(SSL_CTX *ctx, const char *crt) { mbedtls_ssl_config *conf; int rc; if (ctx == NULL || crt == NULL) { return -1; } DEBUG_TRACE("%s", "Initializing MbedTLS SSL"); mbedtls_entropy_init(&ctx->entropy); conf = &ctx->conf; mbedtls_ssl_config_init(conf); /* Set mbedTLS debug level by defining MG_CONFIG_MBEDTLS_DEBUG: * 0 No debug = mbedTLS DEFAULT * 1 Error (default if "DEBUG" is set for CivetWeb) * 2 State change * 3 Informational * 4 Verbose */ #if defined(DEBUG) || defined(MG_CONFIG_MBEDTLS_DEBUG) #if defined(MG_CONFIG_MBEDTLS_DEBUG) mbedtls_debug_set_threshold(MG_CONFIG_MBEDTLS_DEBUG); #else mbedtls_debug_set_threshold(1); #endif mbedtls_ssl_conf_dbg(conf, mbed_debug, (void *)ctx); #endif /* Initialize TLS key and cert */ mbedtls_pk_init(&ctx->pkey); mbedtls_ctr_drbg_init(&ctx->ctr); mbedtls_x509_crt_init(&ctx->cert); rc = mbedtls_ctr_drbg_seed(&ctx->ctr, mbedtls_entropy_func, &ctx->entropy, (unsigned char *)"CivetWeb", strlen("CivetWeb")); if (rc != 0) { DEBUG_TRACE("TLS random seed failed (%i)", rc); return -1; } #if MBEDTLS_VERSION_NUMBER >= 0x03000000 // mbedtls_pk_parse_keyfile() has changed in mbedTLS 3.0. You now need // to pass a properly seeded, cryptographically secure RNG when calling // these functions. It is used for blinding, a countermeasure against // side-channel attacks. // https://github.com/Mbed-TLS/mbedtls/blob/development/docs/3.0-migration-guide.md#some-functions-gained-an-rng-parameter rc = mbedtls_pk_parse_keyfile( &ctx->pkey, crt, NULL, mbedtls_ctr_drbg_random, &ctx->ctr); #else rc = mbedtls_pk_parse_keyfile(&ctx->pkey, crt, NULL); #endif if (rc != 0) { DEBUG_TRACE("TLS parse key file failed (%i)", rc); return -1; } rc = mbedtls_x509_crt_parse_file(&ctx->cert, crt); if (rc != 0) { DEBUG_TRACE("TLS parse crt file failed (%i)", rc); return -1; } rc = mbedtls_ssl_config_defaults(conf, MBEDTLS_SSL_IS_SERVER, MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT); if (rc != 0) { DEBUG_TRACE("TLS set defaults failed (%i)", rc); return -1; } mbedtls_ssl_conf_rng(conf, mbedtls_ctr_drbg_random, &ctx->ctr); /* Set auth mode if peer cert should be verified */ mbedtls_ssl_conf_authmode(conf, MBEDTLS_SSL_VERIFY_NONE); mbedtls_ssl_conf_ca_chain(conf, NULL, NULL); /* Configure server cert and key */ rc = mbedtls_ssl_conf_own_cert(conf, &ctx->cert, &ctx->pkey); if (rc != 0) { DEBUG_TRACE("TLS cannot set certificate and private key (%i)", rc); return -1; } return 0; } void mbed_sslctx_uninit(SSL_CTX *ctx) { mbedtls_ctr_drbg_free(&ctx->ctr); mbedtls_pk_free(&ctx->pkey); mbedtls_x509_crt_free(&ctx->cert); mbedtls_entropy_free(&ctx->entropy); mbedtls_ssl_config_free(&ctx->conf); } int mbed_ssl_accept(mbedtls_ssl_context **ssl, SSL_CTX *ssl_ctx, int *sock, struct mg_context *phys_ctx) { int rc; (void)phys_ctx; /* unused, if server statistics is not turned on */ DEBUG_TRACE("TLS accept processing %p", ssl); *ssl = (mbedtls_ssl_context *)mg_calloc_ctx(1, sizeof(mbedtls_ssl_context), phys_ctx); if (*ssl == NULL) { DEBUG_TRACE("TLS accept: malloc ssl failed (%i)", (int)sizeof(mbedtls_ssl_context)); return -1; } mbedtls_ssl_init(*ssl); mbedtls_ssl_setup(*ssl, &ssl_ctx->conf); mbedtls_ssl_set_bio(*ssl, sock, mbedtls_net_send, mbedtls_net_recv, NULL); rc = mbed_ssl_handshake(*ssl); if (rc != 0) { DEBUG_TRACE("TLS handshake failed (%i)", rc); mbedtls_ssl_free(*ssl); mg_free(*ssl); *ssl = NULL; return -1; } DEBUG_TRACE("TLS connection %p accepted, state: %d", ssl, (*ssl)->state); return 0; } void mbed_ssl_close(mbedtls_ssl_context *ssl) { DEBUG_TRACE("TLS connection %p closed", ssl); mbedtls_ssl_close_notify(ssl); mbedtls_ssl_free(ssl); mg_free(ssl); /* mg_free for mg_calloc in mbed_ssl_accept */ } static int mbed_ssl_handshake(mbedtls_ssl_context *ssl) { int rc; while ((rc = mbedtls_ssl_handshake(ssl)) != 0) { if (rc != MBEDTLS_ERR_SSL_WANT_READ && rc != MBEDTLS_ERR_SSL_WANT_WRITE && rc != MBEDTLS_ERR_SSL_ASYNC_IN_PROGRESS) { break; } } DEBUG_TRACE("TLS handshake rc: %d, state: %d", rc, ssl->state); return rc; } int mbed_ssl_read(mbedtls_ssl_context *ssl, unsigned char *buf, int len) { int rc = mbedtls_ssl_read(ssl, buf, len); /* DEBUG_TRACE("mbedtls_ssl_read: %d", rc); */ return rc; } int mbed_ssl_write(mbedtls_ssl_context *ssl, const unsigned char *buf, int len) { int rc = mbedtls_ssl_write(ssl, buf, len); /* DEBUG_TRACE("mbedtls_ssl_write: %d", rc); */ return rc; } static void mbed_debug(void *user_param, int level, const char *file, int line, const char *str) { (void)level; /* Ignored. Limit is set using mbedtls_debug_set_threshold */ (void)user_param; /* Ignored. User parameter (context) is set using mbedtls_ssl_conf_dbg */ DEBUG_TRACE("mbedTLS DEBUG: file: [%s] line: [%d] str: [%s]", file, line, str); } #endif /* USE_MBEDTLS */ webfakes/src/sort.h0000644000176200001440000000251414740430263013775 0ustar liggesusers/* Sort function. */ /* from https://github.com/bel2125/sort_r */ static void mg_sort(void *data, size_t elemcount, size_t elemsize, int (*compfunc)(const void *data1, const void *data2, void *userarg), void *userarg) { /* We cannot use qsort_r here. For a detailed reason, see * https://github.com/civetweb/civetweb/issues/1048#issuecomment-1047093014 * https://stackoverflow.com/questions/39560773/different-declarations-of-qsort-r-on-mac-and-linux */ /* We use ShellSort here with this gap sequence: https://oeis.org/A102549 */ size_t A102549[9] = {1, 4, 10, 23, 57, 132, 301, 701, 1750}; size_t gap, i, j, k; int Aidx; void *tmp = alloca(elemsize); for (Aidx = 8; Aidx >= 0; Aidx--) { gap = A102549[Aidx]; if (gap > (elemcount / 2)) { continue; } for (i = 0; i < gap; i++) { for (j = i; j < elemcount; j += gap) { memcpy(tmp, (void *)((size_t)data + elemsize * j), elemsize); for (k = j; k >= gap; k -= gap) { void *cmp = (void *)((size_t)data + elemsize * (k - gap)); int cmpres = compfunc(cmp, tmp, userarg); if (cmpres > 0) { memcpy((void *)((size_t)data + elemsize * k), cmp, elemsize); } else { break; } } memcpy((void *)((size_t)data + elemsize * k), tmp, elemsize); } } } } /* end if sort.inl */ webfakes/src/Makevars0000644000176200001440000000011614740430263014325 0ustar liggesusers # PKG_CPPFLAGS=-UNDEBUG PKG_CPPFLAGS=-DNO_SSL PKG_CFLAGS=-DNO_CGI -DNO_FILES webfakes/src/handle_form.h0000644000176200001440000007511414740430263015272 0ustar liggesusers/* Copyright (c) 2016-2021 the Civetweb developers * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ static int url_encoded_field_found(const struct mg_connection *conn, const char *key, size_t key_len, const char *filename, size_t filename_len, char *path, size_t path_len, struct mg_form_data_handler *fdh) { char key_dec[1024]; char filename_dec[1024]; int key_dec_len; int filename_dec_len; int ret; key_dec_len = mg_url_decode(key, (int)key_len, key_dec, (int)sizeof(key_dec), 1); if (((size_t)key_dec_len >= (size_t)sizeof(key_dec)) || (key_dec_len < 0)) { return MG_FORM_FIELD_STORAGE_SKIP; } if (filename) { filename_dec_len = mg_url_decode(filename, (int)filename_len, filename_dec, (int)sizeof(filename_dec), 1); if (((size_t)filename_dec_len >= (size_t)sizeof(filename_dec)) || (filename_dec_len < 0)) { /* Log error message and skip this field. */ mg_cry_internal(conn, "%s: Cannot decode filename", __func__); return MG_FORM_FIELD_STORAGE_SKIP; } remove_dot_segments(filename_dec); } else { filename_dec[0] = 0; } ret = fdh->field_found(key_dec, filename_dec, path, path_len, fdh->user_data); if ((ret & 0xF) == MG_FORM_FIELD_STORAGE_GET) { if (fdh->field_get == NULL) { mg_cry_internal(conn, "%s: Function \"Get\" not available", __func__); return MG_FORM_FIELD_STORAGE_SKIP; } } if ((ret & 0xF) == MG_FORM_FIELD_STORAGE_STORE) { if (fdh->field_store == NULL) { mg_cry_internal(conn, "%s: Function \"Store\" not available", __func__); return MG_FORM_FIELD_STORAGE_SKIP; } } return ret; } static int url_encoded_field_get( const struct mg_connection *conn, const char *key, size_t key_len, const char *value, size_t *value_len, /* IN: number of bytes available in "value", OUT: number of bytes processed */ struct mg_form_data_handler *fdh) { char key_dec[1024]; char *value_dec = (char *)mg_malloc_ctx(*value_len + 1, conn->phys_ctx); int value_dec_len, ret; if (!value_dec) { /* Log error message and stop parsing the form data. */ mg_cry_internal(conn, "%s: Not enough memory (required: %lu)", __func__, (unsigned long)(*value_len + 1)); return MG_FORM_FIELD_STORAGE_ABORT; } mg_url_decode(key, (int)key_len, key_dec, (int)sizeof(key_dec), 1); if (*value_len >= 2 && value[*value_len - 2] == '%') *value_len -= 2; else if (*value_len >= 1 && value[*value_len - 1] == '%') (*value_len)--; value_dec_len = mg_url_decode( value, (int)*value_len, value_dec, ((int)*value_len) + 1, 1); ret = fdh->field_get(key_dec, value_dec, (size_t)value_dec_len, fdh->user_data); mg_free(value_dec); return ret; } static int unencoded_field_get(const struct mg_connection *conn, const char *key, size_t key_len, const char *value, size_t value_len, struct mg_form_data_handler *fdh) { char key_dec[1024]; (void)conn; mg_url_decode(key, (int)key_len, key_dec, (int)sizeof(key_dec), 1); return fdh->field_get(key_dec, value, value_len, fdh->user_data); } static int field_stored(const struct mg_connection *conn, const char *path, long long file_size, struct mg_form_data_handler *fdh) { /* Equivalent to "upload" callback of "mg_upload". */ (void)conn; /* we do not need mg_cry here, so conn is currently unused */ return fdh->field_store(path, file_size, fdh->user_data); } static const char * search_boundary(const char *buf, size_t buf_len, const char *boundary, size_t boundary_len) { /* We must do a binary search here, not a string search, since the buffer * may contain '\x00' bytes, if binary data is transferred. */ int clen = (int)buf_len - (int)boundary_len - 4; int i; for (i = 0; i <= clen; i++) { if (!memcmp(buf + i, "\r\n--", 4)) { if (!memcmp(buf + i + 4, boundary, boundary_len)) { return buf + i; } } } return NULL; } int mg_handle_form_request(struct mg_connection *conn, struct mg_form_data_handler *fdh) { const char *content_type; char path[512]; char buf[MG_BUF_LEN]; /* Must not be smaller than ~900 */ int field_storage; int buf_fill = 0; int r; int field_count = 0; struct mg_file fstore = STRUCT_FILE_INITIALIZER; int64_t file_size = 0; /* init here, to a avoid a false positive "uninitialized variable used" warning */ int has_body_data = (conn->request_info.content_length > 0) || (conn->is_chunked); /* Unused without filesystems */ (void)fstore; (void)file_size; /* There are three ways to encode data from a HTML form: * 1) method: GET (default) * The form data is in the HTTP query string. * 2) method: POST, enctype: "application/x-www-form-urlencoded" * The form data is in the request body. * The body is url encoded (the default encoding for POST). * 3) method: POST, enctype: "multipart/form-data". * The form data is in the request body of a multipart message. * This is the typical way to handle file upload from a form. */ if (!has_body_data) { const char *data; if (0 != strcmp(conn->request_info.request_method, "GET")) { /* No body data, but not a GET request. * This is not a valid form request. */ return -1; } /* GET request: form data is in the query string. */ /* The entire data has already been loaded, so there is no need to * call mg_read. We just need to split the query string into key-value * pairs. */ data = conn->request_info.query_string; if (!data) { /* No query string. */ return -1; } /* Split data in a=1&b=xy&c=3&c=4 ... */ while (*data) { const char *val = strchr(data, '='); const char *next; ptrdiff_t keylen, vallen; if (!val) { break; } keylen = val - data; /* In every "field_found" callback we ask what to do with the * data ("field_storage"). This could be: * MG_FORM_FIELD_STORAGE_SKIP (0): * ignore the value of this field * MG_FORM_FIELD_STORAGE_GET (1): * read the data and call the get callback function * MG_FORM_FIELD_STORAGE_STORE (2): * store the data in a file * MG_FORM_FIELD_STORAGE_READ (3): * let the user read the data (for parsing long data on the fly) * MG_FORM_FIELD_STORAGE_ABORT (flag): * stop parsing */ memset(path, 0, sizeof(path)); field_count++; field_storage = url_encoded_field_found(conn, data, (size_t)keylen, NULL, 0, path, sizeof(path) - 1, fdh); val++; next = strchr(val, '&'); if (next) { vallen = next - val; } else { vallen = (ptrdiff_t)strlen(val); } if (field_storage == MG_FORM_FIELD_STORAGE_GET) { /* Call callback */ r = url_encoded_field_get( conn, data, (size_t)keylen, val, (size_t *)&vallen, fdh); if (r == MG_FORM_FIELD_HANDLE_ABORT) { /* Stop request handling */ break; } if (r == MG_FORM_FIELD_HANDLE_NEXT) { /* Skip to next field */ field_storage = MG_FORM_FIELD_STORAGE_SKIP; } } if (next) { next++; } else { /* vallen may have been modified by url_encoded_field_get */ next = val + vallen; } #if !defined(NO_FILESYSTEMS) if (field_storage == MG_FORM_FIELD_STORAGE_STORE) { /* Store the content to a file */ if (mg_fopen(conn, path, MG_FOPEN_MODE_WRITE, &fstore) == 0) { fstore.access.fp = NULL; } file_size = 0; if (fstore.access.fp != NULL) { size_t n = (size_t) fwrite(val, 1, (size_t)vallen, fstore.access.fp); if ((n != (size_t)vallen) || (ferror(fstore.access.fp))) { mg_cry_internal(conn, "%s: Cannot write file %s", __func__, path); (void)mg_fclose(&fstore.access); remove_bad_file(conn, path); } file_size += (int64_t)n; if (fstore.access.fp) { r = mg_fclose(&fstore.access); if (r == 0) { /* stored successfully */ r = field_stored(conn, path, file_size, fdh); if (r == MG_FORM_FIELD_HANDLE_ABORT) { /* Stop request handling */ break; } } else { mg_cry_internal(conn, "%s: Error saving file %s", __func__, path); remove_bad_file(conn, path); } fstore.access.fp = NULL; } } else { mg_cry_internal(conn, "%s: Cannot create file %s", __func__, path); } } #endif /* NO_FILESYSTEMS */ /* if (field_storage == MG_FORM_FIELD_STORAGE_READ) { */ /* The idea of "field_storage=read" is to let the API user read * data chunk by chunk and to some data processing on the fly. * This should avoid the need to store data in the server: * It should neither be stored in memory, like * "field_storage=get" does, nor in a file like * "field_storage=store". * However, for a "GET" request this does not make any much * sense, since the data is already stored in memory, as it is * part of the query string. */ /* } */ if ((field_storage & MG_FORM_FIELD_STORAGE_ABORT) == MG_FORM_FIELD_STORAGE_ABORT) { /* Stop parsing the request */ break; } /* Proceed to next entry */ data = next; } return field_count; } content_type = mg_get_header(conn, "Content-Type"); if (!content_type || !mg_strncasecmp(content_type, "APPLICATION/X-WWW-FORM-URLENCODED", 33) || !mg_strncasecmp(content_type, "APPLICATION/WWW-FORM-URLENCODED", 31)) { /* The form data is in the request body data, encoded in key/value * pairs. */ int all_data_read = 0; /* Read body data and split it in keys and values. * The encoding is like in the "GET" case above: a=1&b&c=3&c=4. * Here we use "POST", and read the data from the request body. * The data read on the fly, so it is not required to buffer the * entire request in memory before processing it. */ for (;;) { const char *val; const char *next; ptrdiff_t keylen, vallen; ptrdiff_t used; int end_of_key_value_pair_found = 0; int get_block; if ((size_t)buf_fill < (sizeof(buf) - 1)) { size_t to_read = sizeof(buf) - 1 - (size_t)buf_fill; r = mg_read(conn, buf + (size_t)buf_fill, to_read); if ((r < 0) || ((r == 0) && all_data_read)) { /* read error */ return -1; } if (r == 0) { /* TODO: Create a function to get "all_data_read" from * the conn object. All data is read if the Content-Length * has been reached, or if chunked encoding is used and * the end marker has been read, or if the connection has * been closed. */ all_data_read = (buf_fill == 0); } buf_fill += r; buf[buf_fill] = 0; if (buf_fill < 1) { break; } } val = strchr(buf, '='); if (!val) { break; } keylen = val - buf; val++; /* Call callback */ memset(path, 0, sizeof(path)); field_count++; field_storage = url_encoded_field_found(conn, buf, (size_t)keylen, NULL, 0, path, sizeof(path) - 1, fdh); if ((field_storage & MG_FORM_FIELD_STORAGE_ABORT) == MG_FORM_FIELD_STORAGE_ABORT) { /* Stop parsing the request */ break; } #if !defined(NO_FILESYSTEMS) if (field_storage == MG_FORM_FIELD_STORAGE_STORE) { if (mg_fopen(conn, path, MG_FOPEN_MODE_WRITE, &fstore) == 0) { fstore.access.fp = NULL; } file_size = 0; if (!fstore.access.fp) { mg_cry_internal(conn, "%s: Cannot create file %s", __func__, path); } } #endif /* NO_FILESYSTEMS */ get_block = 0; /* Loop to read values larger than sizeof(buf)-keylen-2 */ do { next = strchr(val, '&'); if (next) { vallen = next - val; end_of_key_value_pair_found = 1; } else { vallen = (ptrdiff_t)strlen(val); end_of_key_value_pair_found = all_data_read; } if (field_storage == MG_FORM_FIELD_STORAGE_GET) { #if 0 if (!end_of_key_value_pair_found && !all_data_read) { /* This callback will deliver partial contents */ } #endif /* Call callback */ r = url_encoded_field_get(conn, ((get_block > 0) ? NULL : buf), ((get_block > 0) ? 0 : (size_t)keylen), val, (size_t *)&vallen, fdh); get_block++; if (r == MG_FORM_FIELD_HANDLE_ABORT) { /* Stop request handling */ break; } if (r == MG_FORM_FIELD_HANDLE_NEXT) { /* Skip to next field */ field_storage = MG_FORM_FIELD_STORAGE_SKIP; } } if (next) { next++; } else { /* vallen may have been modified by url_encoded_field_get */ next = val + vallen; } #if !defined(NO_FILESYSTEMS) if (fstore.access.fp) { size_t n = (size_t) fwrite(val, 1, (size_t)vallen, fstore.access.fp); if ((n != (size_t)vallen) || (ferror(fstore.access.fp))) { mg_cry_internal(conn, "%s: Cannot write file %s", __func__, path); mg_fclose(&fstore.access); remove_bad_file(conn, path); } file_size += (int64_t)n; } #endif /* NO_FILESYSTEMS */ if (!end_of_key_value_pair_found) { used = next - buf; memmove(buf, buf + (size_t)used, sizeof(buf) - (size_t)used); next = buf; buf_fill -= (int)used; if ((size_t)buf_fill < (sizeof(buf) - 1)) { size_t to_read = sizeof(buf) - 1 - (size_t)buf_fill; r = mg_read(conn, buf + (size_t)buf_fill, to_read); if ((r < 0) || ((r == 0) && all_data_read)) { #if !defined(NO_FILESYSTEMS) /* read error */ if (fstore.access.fp) { mg_fclose(&fstore.access); remove_bad_file(conn, path); } return -1; #endif /* NO_FILESYSTEMS */ } if (r == 0) { /* TODO: Create a function to get "all_data_read" * from the conn object. All data is read if the * Content-Length has been reached, or if chunked * encoding is used and the end marker has been * read, or if the connection has been closed. */ all_data_read = (buf_fill == 0); } buf_fill += r; buf[buf_fill] = 0; if (buf_fill < 1) { break; } val = buf; } } } while (!end_of_key_value_pair_found); #if !defined(NO_FILESYSTEMS) if (fstore.access.fp) { r = mg_fclose(&fstore.access); if (r == 0) { /* stored successfully */ r = field_stored(conn, path, file_size, fdh); if (r == MG_FORM_FIELD_HANDLE_ABORT) { /* Stop request handling */ break; } } else { mg_cry_internal(conn, "%s: Error saving file %s", __func__, path); remove_bad_file(conn, path); } fstore.access.fp = NULL; } #endif /* NO_FILESYSTEMS */ if (all_data_read && (buf_fill == 0)) { /* nothing more to process */ break; } /* Proceed to next entry */ used = next - buf; memmove(buf, buf + (size_t)used, sizeof(buf) - (size_t)used); buf_fill -= (int)used; } return field_count; } if (!mg_strncasecmp(content_type, "MULTIPART/FORM-DATA;", 20)) { /* The form data is in the request body data, encoded as multipart * content (see https://www.ietf.org/rfc/rfc1867.txt, * https://www.ietf.org/rfc/rfc2388.txt). */ char *boundary; size_t bl; ptrdiff_t used; struct mg_request_info part_header; char *hbuf; const char *content_disp, *hend, *fbeg, *fend, *nbeg, *nend; const char *next; unsigned part_no; int all_data_read = 0; memset(&part_header, 0, sizeof(part_header)); /* Skip all spaces between MULTIPART/FORM-DATA; and BOUNDARY= */ bl = 20; while (content_type[bl] == ' ') { bl++; } /* There has to be a BOUNDARY definition in the Content-Type header */ if (mg_strncasecmp(content_type + bl, "BOUNDARY=", 9)) { /* Malformed request */ return -1; } /* Copy boundary string to variable "boundary" */ fbeg = content_type + bl + 9; bl = strlen(fbeg); boundary = (char *)mg_malloc(bl + 1); if (!boundary) { /* Out of memory */ mg_cry_internal(conn, "%s: Cannot allocate memory for boundary [%lu]", __func__, (unsigned long)bl); return -1; } memcpy(boundary, fbeg, bl); boundary[bl] = 0; /* RFC 2046 permits the boundary string to be quoted. */ /* If the boundary is quoted, trim the quotes */ if (boundary[0] == '"') { hbuf = strchr(boundary + 1, '"'); if ((!hbuf) || (*hbuf != '"')) { /* Malformed request */ mg_free(boundary); return -1; } *hbuf = 0; memmove(boundary, boundary + 1, bl); bl = strlen(boundary); } /* Do some sanity checks for boundary lengths */ if (bl > 70) { /* From RFC 2046: * Boundary delimiters must not appear within the * encapsulated material, and must be no longer * than 70 characters, not counting the two * leading hyphens. */ /* The algorithm can not work if bl >= sizeof(buf), or if buf * can not hold the multipart header plus the boundary. * Requests with long boundaries are not RFC compliant, maybe they * are intended attacks to interfere with this algorithm. */ mg_free(boundary); return -1; } if (bl < 4) { /* Sanity check: A boundary string of less than 4 bytes makes * no sense either. */ mg_free(boundary); return -1; } for (part_no = 0;; part_no++) { size_t towrite, fnlen, n; int get_block; size_t to_read = sizeof(buf) - 1 - (size_t)buf_fill; /* Unused without filesystems */ (void)n; r = mg_read(conn, buf + (size_t)buf_fill, to_read); if ((r < 0) || ((r == 0) && all_data_read)) { /* read error */ mg_free(boundary); return -1; } if (r == 0) { all_data_read = (buf_fill == 0); } buf_fill += r; buf[buf_fill] = 0; if (buf_fill < 1) { /* No data */ mg_free(boundary); return -1; } if (part_no == 0) { int d = 0; while ((d < buf_fill) && (buf[d] != '-')) { d++; } if ((d > 0) && (buf[d] == '-')) { memmove(buf, buf + d, (unsigned)buf_fill - (unsigned)d); buf_fill -= d; buf[buf_fill] = 0; } } if (buf[0] != '-' || buf[1] != '-') { /* Malformed request */ mg_free(boundary); return -1; } if (0 != strncmp(buf + 2, boundary, bl)) { /* Malformed request */ mg_free(boundary); return -1; } if (buf[bl + 2] != '\r' || buf[bl + 3] != '\n') { /* Every part must end with \r\n, if there is another part. * The end of the request has an extra -- */ if (((size_t)buf_fill != (size_t)(bl + 6)) || (strncmp(buf + bl + 2, "--\r\n", 4))) { /* Malformed request */ mg_free(boundary); return -1; } /* End of the request */ break; } /* Next, we need to get the part header: Read until \r\n\r\n */ hbuf = buf + bl + 4; hend = strstr(hbuf, "\r\n\r\n"); if (!hend) { /* Malformed request */ mg_free(boundary); return -1; } part_header.num_headers = parse_http_headers(&hbuf, part_header.http_headers); if ((hend + 2) != hbuf) { /* Malformed request */ mg_free(boundary); return -1; } /* Skip \r\n\r\n */ hend += 4; /* According to the RFC, every part has to have a header field like: * Content-Disposition: form-data; name="..." */ content_disp = get_header(part_header.http_headers, part_header.num_headers, "Content-Disposition"); if (!content_disp) { /* Malformed request */ mg_free(boundary); return -1; } /* Get the mandatory name="..." part of the Content-Disposition * header. */ nbeg = strstr(content_disp, "name=\""); while ((nbeg != NULL) && (strcspn(nbeg - 1, ":,; \t") != 0)) { /* It could be somethingname= instead of name= */ nbeg = strstr(nbeg + 1, "name=\""); } /* This line is not required, but otherwise some compilers * generate spurious warnings. */ nend = nbeg; /* And others complain, the result is unused. */ (void)nend; /* If name=" is found, search for the closing " */ if (nbeg) { nbeg += 6; nend = strchr(nbeg, '\"'); if (!nend) { /* Malformed request */ mg_free(boundary); return -1; } } else { /* name= without quotes is also allowed */ nbeg = strstr(content_disp, "name="); while ((nbeg != NULL) && (strcspn(nbeg - 1, ":,; \t") != 0)) { /* It could be somethingname= instead of name= */ nbeg = strstr(nbeg + 1, "name="); } if (!nbeg) { /* Malformed request */ mg_free(boundary); return -1; } nbeg += 5; /* RFC 2616 Sec. 2.2 defines a list of allowed * separators, but many of them make no sense * here, e.g. various brackets or slashes. * If they are used, probably someone is * trying to attack with curious hand made * requests. Only ; , space and tab seem to be * reasonable here. Ignore everything else. */ nend = nbeg + strcspn(nbeg, ",; \t"); } /* Get the optional filename="..." part of the Content-Disposition * header. */ fbeg = strstr(content_disp, "filename=\""); while ((fbeg != NULL) && (strcspn(fbeg - 1, ":,; \t") != 0)) { /* It could be somethingfilename= instead of filename= */ fbeg = strstr(fbeg + 1, "filename=\""); } /* This line is not required, but otherwise some compilers * generate spurious warnings. */ fend = fbeg; /* If filename=" is found, search for the closing " */ if (fbeg) { fbeg += 10; fend = strchr(fbeg, '\"'); if (!fend) { /* Malformed request (the filename field is optional, but if * it exists, it needs to be terminated correctly). */ mg_free(boundary); return -1; } /* TODO: check Content-Type */ /* Content-Type: application/octet-stream */ } if (!fbeg) { /* Try the same without quotes */ fbeg = strstr(content_disp, "filename="); while ((fbeg != NULL) && (strcspn(fbeg - 1, ":,; \t") != 0)) { /* It could be somethingfilename= instead of filename= */ fbeg = strstr(fbeg + 1, "filename="); } if (fbeg) { fbeg += 9; fend = fbeg + strcspn(fbeg, ",; \t"); } } if (!fbeg || !fend) { fbeg = NULL; fend = NULL; fnlen = 0; } else { fnlen = (size_t)(fend - fbeg); } /* In theory, it could be possible that someone crafts * a request like name=filename=xyz. Check if name and * filename do not overlap. */ if (!(((ptrdiff_t)fbeg > (ptrdiff_t)nend) || ((ptrdiff_t)nbeg > (ptrdiff_t)fend))) { mg_free(boundary); return -1; } /* Call callback for new field */ memset(path, 0, sizeof(path)); field_count++; field_storage = url_encoded_field_found(conn, nbeg, (size_t)(nend - nbeg), ((fnlen > 0) ? fbeg : NULL), fnlen, path, sizeof(path) - 1, fdh); /* If the boundary is already in the buffer, get the address, * otherwise next will be NULL. */ next = search_boundary(hbuf, (size_t)((buf - hbuf) + buf_fill), boundary, bl); #if !defined(NO_FILESYSTEMS) if (field_storage == MG_FORM_FIELD_STORAGE_STORE) { /* Store the content to a file */ if (mg_fopen(conn, path, MG_FOPEN_MODE_WRITE, &fstore) == 0) { fstore.access.fp = NULL; } file_size = 0; if (!fstore.access.fp) { mg_cry_internal(conn, "%s: Cannot create file %s", __func__, path); } } #endif /* NO_FILESYSTEMS */ get_block = 0; while (!next) { /* Set "towrite" to the number of bytes available * in the buffer */ towrite = (size_t)(buf - hend + buf_fill); if (towrite < bl + 4) { /* Not enough data stored. */ /* Incomplete request. */ mg_free(boundary); return -1; } /* Subtract the boundary length, to deal with * cases the boundary is only partially stored * in the buffer. */ towrite -= bl + 4; if (field_storage == MG_FORM_FIELD_STORAGE_GET) { r = unencoded_field_get(conn, ((get_block > 0) ? NULL : nbeg), ((get_block > 0) ? 0 : (size_t)(nend - nbeg)), hend, towrite, fdh); get_block++; if (r == MG_FORM_FIELD_HANDLE_ABORT) { /* Stop request handling */ break; } if (r == MG_FORM_FIELD_HANDLE_NEXT) { /* Skip to next field */ field_storage = MG_FORM_FIELD_STORAGE_SKIP; } } #if !defined(NO_FILESYSTEMS) if (field_storage == MG_FORM_FIELD_STORAGE_STORE) { if (fstore.access.fp) { /* Store the content of the buffer. */ n = (size_t)fwrite(hend, 1, towrite, fstore.access.fp); if ((n != towrite) || (ferror(fstore.access.fp))) { mg_cry_internal(conn, "%s: Cannot write file %s", __func__, path); mg_fclose(&fstore.access); remove_bad_file(conn, path); } file_size += (int64_t)n; } } #endif /* NO_FILESYSTEMS */ memmove(buf, hend + towrite, bl + 4); buf_fill = (int)(bl + 4); hend = buf; /* Read new data */ to_read = sizeof(buf) - 1 - (size_t)buf_fill; r = mg_read(conn, buf + (size_t)buf_fill, to_read); if ((r < 0) || ((r == 0) && all_data_read)) { #if !defined(NO_FILESYSTEMS) /* read error */ if (fstore.access.fp) { mg_fclose(&fstore.access); remove_bad_file(conn, path); } #endif /* NO_FILESYSTEMS */ mg_free(boundary); return -1; } /* r==0 already handled, all_data_read is false here */ buf_fill += r; buf[buf_fill] = 0; /* buf_fill is at least 8 here */ /* Find boundary */ next = search_boundary(buf, (size_t)buf_fill, boundary, bl); if (!next && (r == 0)) { /* incomplete request */ all_data_read = 1; } } towrite = (next ? (size_t)(next - hend) : 0); if (field_storage == MG_FORM_FIELD_STORAGE_GET) { /* Call callback */ r = unencoded_field_get(conn, ((get_block > 0) ? NULL : nbeg), ((get_block > 0) ? 0 : (size_t)(nend - nbeg)), hend, towrite, fdh); if (r == MG_FORM_FIELD_HANDLE_ABORT) { /* Stop request handling */ break; } if (r == MG_FORM_FIELD_HANDLE_NEXT) { /* Skip to next field */ field_storage = MG_FORM_FIELD_STORAGE_SKIP; } } #if !defined(NO_FILESYSTEMS) if (field_storage == MG_FORM_FIELD_STORAGE_STORE) { if (fstore.access.fp) { n = (size_t)fwrite(hend, 1, towrite, fstore.access.fp); if ((n != towrite) || (ferror(fstore.access.fp))) { mg_cry_internal(conn, "%s: Cannot write file %s", __func__, path); mg_fclose(&fstore.access); remove_bad_file(conn, path); } else { file_size += (int64_t)n; r = mg_fclose(&fstore.access); if (r == 0) { /* stored successfully */ r = field_stored(conn, path, file_size, fdh); if (r == MG_FORM_FIELD_HANDLE_ABORT) { /* Stop request handling */ break; } } else { mg_cry_internal(conn, "%s: Error saving file %s", __func__, path); remove_bad_file(conn, path); } } fstore.access.fp = NULL; } } #endif /* NO_FILESYSTEMS */ if ((field_storage & MG_FORM_FIELD_STORAGE_ABORT) == MG_FORM_FIELD_STORAGE_ABORT) { /* Stop parsing the request */ break; } /* Remove from the buffer */ if (next) { used = next - buf + 2; memmove(buf, buf + (size_t)used, sizeof(buf) - (size_t)used); buf_fill -= (int)used; } else { buf_fill = 0; } } /* All parts handled */ mg_free(boundary); return field_count; } /* Unknown Content-Type */ return -1; } /* End of handle_form.inl */ webfakes/src/crc32.c0000644000176200001440000001021414172041777013720 0ustar liggesusers #include #include static uint32_t crc32_tab[] = { 0x00000000L, 0x77073096L, 0xee0e612cL, 0x990951baL, 0x076dc419L, 0x706af48fL, 0xe963a535L, 0x9e6495a3L, 0x0edb8832L, 0x79dcb8a4L, 0xe0d5e91eL, 0x97d2d988L, 0x09b64c2bL, 0x7eb17cbdL, 0xe7b82d07L, 0x90bf1d91L, 0x1db71064L, 0x6ab020f2L, 0xf3b97148L, 0x84be41deL, 0x1adad47dL, 0x6ddde4ebL, 0xf4d4b551L, 0x83d385c7L, 0x136c9856L, 0x646ba8c0L, 0xfd62f97aL, 0x8a65c9ecL, 0x14015c4fL, 0x63066cd9L, 0xfa0f3d63L, 0x8d080df5L, 0x3b6e20c8L, 0x4c69105eL, 0xd56041e4L, 0xa2677172L, 0x3c03e4d1L, 0x4b04d447L, 0xd20d85fdL, 0xa50ab56bL, 0x35b5a8faL, 0x42b2986cL, 0xdbbbc9d6L, 0xacbcf940L, 0x32d86ce3L, 0x45df5c75L, 0xdcd60dcfL, 0xabd13d59L, 0x26d930acL, 0x51de003aL, 0xc8d75180L, 0xbfd06116L, 0x21b4f4b5L, 0x56b3c423L, 0xcfba9599L, 0xb8bda50fL, 0x2802b89eL, 0x5f058808L, 0xc60cd9b2L, 0xb10be924L, 0x2f6f7c87L, 0x58684c11L, 0xc1611dabL, 0xb6662d3dL, 0x76dc4190L, 0x01db7106L, 0x98d220bcL, 0xefd5102aL, 0x71b18589L, 0x06b6b51fL, 0x9fbfe4a5L, 0xe8b8d433L, 0x7807c9a2L, 0x0f00f934L, 0x9609a88eL, 0xe10e9818L, 0x7f6a0dbbL, 0x086d3d2dL, 0x91646c97L, 0xe6635c01L, 0x6b6b51f4L, 0x1c6c6162L, 0x856530d8L, 0xf262004eL, 0x6c0695edL, 0x1b01a57bL, 0x8208f4c1L, 0xf50fc457L, 0x65b0d9c6L, 0x12b7e950L, 0x8bbeb8eaL, 0xfcb9887cL, 0x62dd1ddfL, 0x15da2d49L, 0x8cd37cf3L, 0xfbd44c65L, 0x4db26158L, 0x3ab551ceL, 0xa3bc0074L, 0xd4bb30e2L, 0x4adfa541L, 0x3dd895d7L, 0xa4d1c46dL, 0xd3d6f4fbL, 0x4369e96aL, 0x346ed9fcL, 0xad678846L, 0xda60b8d0L, 0x44042d73L, 0x33031de5L, 0xaa0a4c5fL, 0xdd0d7cc9L, 0x5005713cL, 0x270241aaL, 0xbe0b1010L, 0xc90c2086L, 0x5768b525L, 0x206f85b3L, 0xb966d409L, 0xce61e49fL, 0x5edef90eL, 0x29d9c998L, 0xb0d09822L, 0xc7d7a8b4L, 0x59b33d17L, 0x2eb40d81L, 0xb7bd5c3bL, 0xc0ba6cadL, 0xedb88320L, 0x9abfb3b6L, 0x03b6e20cL, 0x74b1d29aL, 0xead54739L, 0x9dd277afL, 0x04db2615L, 0x73dc1683L, 0xe3630b12L, 0x94643b84L, 0x0d6d6a3eL, 0x7a6a5aa8L, 0xe40ecf0bL, 0x9309ff9dL, 0x0a00ae27L, 0x7d079eb1L, 0xf00f9344L, 0x8708a3d2L, 0x1e01f268L, 0x6906c2feL, 0xf762575dL, 0x806567cbL, 0x196c3671L, 0x6e6b06e7L, 0xfed41b76L, 0x89d32be0L, 0x10da7a5aL, 0x67dd4accL, 0xf9b9df6fL, 0x8ebeeff9L, 0x17b7be43L, 0x60b08ed5L, 0xd6d6a3e8L, 0xa1d1937eL, 0x38d8c2c4L, 0x4fdff252L, 0xd1bb67f1L, 0xa6bc5767L, 0x3fb506ddL, 0x48b2364bL, 0xd80d2bdaL, 0xaf0a1b4cL, 0x36034af6L, 0x41047a60L, 0xdf60efc3L, 0xa867df55L, 0x316e8eefL, 0x4669be79L, 0xcb61b38cL, 0xbc66831aL, 0x256fd2a0L, 0x5268e236L, 0xcc0c7795L, 0xbb0b4703L, 0x220216b9L, 0x5505262fL, 0xc5ba3bbeL, 0xb2bd0b28L, 0x2bb45a92L, 0x5cb36a04L, 0xc2d7ffa7L, 0xb5d0cf31L, 0x2cd99e8bL, 0x5bdeae1dL, 0x9b64c2b0L, 0xec63f226L, 0x756aa39cL, 0x026d930aL, 0x9c0906a9L, 0xeb0e363fL, 0x72076785L, 0x05005713L, 0x95bf4a82L, 0xe2b87a14L, 0x7bb12baeL, 0x0cb61b38L, 0x92d28e9bL, 0xe5d5be0dL, 0x7cdcefb7L, 0x0bdbdf21L, 0x86d3d2d4L, 0xf1d4e242L, 0x68ddb3f8L, 0x1fda836eL, 0x81be16cdL, 0xf6b9265bL, 0x6fb077e1L, 0x18b74777L, 0x88085ae6L, 0xff0f6a70L, 0x66063bcaL, 0x11010b5cL, 0x8f659effL, 0xf862ae69L, 0x616bffd3L, 0x166ccf45L, 0xa00ae278L, 0xd70dd2eeL, 0x4e048354L, 0x3903b3c2L, 0xa7672661L, 0xd06016f7L, 0x4969474dL, 0x3e6e77dbL, 0xaed16a4aL, 0xd9d65adcL, 0x40df0b66L, 0x37d83bf0L, 0xa9bcae53L, 0xdebb9ec5L, 0x47b2cf7fL, 0x30b5ffe9L, 0xbdbdf21cL, 0xcabac28aL, 0x53b39330L, 0x24b4a3a6L, 0xbad03605L, 0xcdd70693L, 0x54de5729L, 0x23d967bfL, 0xb3667a2eL, 0xc4614ab8L, 0x5d681b02L, 0x2a6f2b94L, 0xb40bbe37L, 0xc30c8ea1L, 0x5a05df1bL, 0x2d02ef8dL }; static char hexa[16] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; /* crc32 hash */ SEXP webfakes_crc32(SEXP v) { int len = LENGTH(v); Rbyte *begin = RAW(v), *end = begin + len; uint32_t crc = 0xffffffff; char str[9]; for (; begin < end; begin++) { crc = crc32_tab[(crc ^ *begin) & 0xff] ^ ((crc >> 8) & 0x00ffffff); } crc ^= 0xffffffff; str[0] = hexa[(crc >> 28)]; str[1] = hexa[(crc >> 24) & 0xf]; str[2] = hexa[(crc >> 20) & 0xf]; str[3] = hexa[(crc >> 16) & 0xf]; str[4] = hexa[(crc >> 12) & 0xf]; str[5] = hexa[(crc >> 8) & 0xf]; str[6] = hexa[(crc >> 4) & 0xf]; str[7] = hexa[crc & 0xf]; str[8] = '\0'; return mkString(str); } webfakes/src/sha1.h0000644000176200001440000002117114740430263013642 0ustar liggesusers/* SHA-1 in C By Steve Reid 100% Public Domain ----------------- Modified 7/98 By James H. Brown Still 100% Public Domain Corrected a problem which generated improper hash values on 16 bit machines Routine SHA1Update changed from void SHA1Update(SHA_CTX* context, unsigned char* data, unsigned int len) to void SHA1Update(SHA_CTX* context, unsigned char* data, unsigned long len) The 'len' parameter was declared an int which works fine on 32 bit machines. However, on 16 bit machines an int is too small for the shifts being done against it. This caused the hash function to generate incorrect values if len was greater than 8191 (8K - 1) due to the 'len << 3' on line 3 of SHA1Update(). Since the file IO in main() reads 16K at a time, any file 8K or larger would be guaranteed to generate the wrong hash (e.g. Test Vector #3, a million "a"s). I also changed the declaration of variables i & j in SHA1Update to unsigned long from unsigned int for the same reason. These changes should make no difference to any 32 bit implementations since an int and a long are the same size in those environments. -- I also corrected a few compiler warnings generated by Borland C. 1. Added #include for exit() prototype 2. Removed unused variable 'j' in SHA1Final 3. Changed exit(0) to return(0) at end of main. ALL changes I made can be located by searching for comments containing 'JHB' ----------------- Modified 8/98 By Steve Reid Still 100% public domain 1- Removed #include and used return() instead of exit() 2- Fixed overwriting of finalcount in SHA1Final() (discovered by Chris Hall) 3- Changed email address from steve@edmweb.com to sreid@sea-to-sky.net ----------------- Modified 4/01 By Saul Kravitz Still 100% PD Modified to run on Compaq Alpha hardware. ----------------- Modified 07/2002 By Ralph Giles Still 100% public domain modified for use with stdint types, autoconf code cleanup, removed attribution comments switched SHA1Final() argument order for consistency use SHA1_ prefix for public api move public api to sha1.h */ /* 11/2016 adapted for CivetWeb: include sha1.h in sha1.c, rename to sha1.inl remove unused #ifdef sections make endian independent align buffer to 4 bytes remove unused variable assignments */ /* Test Vectors (from FIPS PUB 180-1) "abc" A9993E36 4706816A BA3E2571 7850C26C 9CD0D89D "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq" 84983E44 1C3BD26E BAAE4AA1 F95129E5 E54670F1 A million repetitions of "a" 34AA973C D4C4DAA4 F61EEB2B DBAD2731 6534016F */ #include #include typedef struct { uint32_t state[5]; uint32_t count[2]; uint8_t buffer[64]; } SHA_CTX; #define SHA1_DIGEST_SIZE 20 #define rol(value, bits) (((value) << (bits)) | ((value) >> (32 - (bits)))) /* blk0() and blk() perform the initial expand. */ /* I got the idea of expanding during the round function from SSLeay */ typedef union { uint8_t c[64]; uint32_t l[16]; } CHAR64LONG16; static uint32_t blk0(CHAR64LONG16 *block, int i) { static const uint32_t n = 1u; if ((*((uint8_t *)(&n))) == 1) { /* little endian / intel byte order */ block->l[i] = (rol(block->l[i], 24) & 0xFF00FF00) | (rol(block->l[i], 8) & 0x00FF00FF); } return block->l[i]; } #define blk(block, i) \ ((block)->l[(i)&15] = \ rol((block)->l[((i) + 13) & 15] ^ (block)->l[((i) + 8) & 15] \ ^ (block)->l[((i) + 2) & 15] ^ (block)->l[(i)&15], \ 1)) /* (R0+R1), R2, R3, R4 are the different operations used in SHA1 */ #define R0(v, w, x, y, z, i) \ z += ((w & (x ^ y)) ^ y) + blk0(block, i) + 0x5A827999 + rol(v, 5); \ w = rol(w, 30); #define R1(v, w, x, y, z, i) \ z += ((w & (x ^ y)) ^ y) + blk(block, i) + 0x5A827999 + rol(v, 5); \ w = rol(w, 30); #define R2(v, w, x, y, z, i) \ z += (w ^ x ^ y) + blk(block, i) + 0x6ED9EBA1 + rol(v, 5); \ w = rol(w, 30); #define R3(v, w, x, y, z, i) \ z += (((w | x) & y) | (w & x)) + blk(block, i) + 0x8F1BBCDC + rol(v, 5); \ w = rol(w, 30); #define R4(v, w, x, y, z, i) \ z += (w ^ x ^ y) + blk(block, i) + 0xCA62C1D6 + rol(v, 5); \ w = rol(w, 30); /* Hash a single 512-bit block. This is the core of the algorithm. */ static void SHA1_Transform(uint32_t state[5], const uint8_t buffer[64]) { uint32_t a, b, c, d, e; /* Must use an aligned, read/write buffer */ CHAR64LONG16 block[1]; memcpy(block, buffer, sizeof(block)); /* Copy context->state[] to working vars */ a = state[0]; b = state[1]; c = state[2]; d = state[3]; e = state[4]; /* 4 rounds of 20 operations each. Loop unrolled. */ R0(a, b, c, d, e, 0); R0(e, a, b, c, d, 1); R0(d, e, a, b, c, 2); R0(c, d, e, a, b, 3); R0(b, c, d, e, a, 4); R0(a, b, c, d, e, 5); R0(e, a, b, c, d, 6); R0(d, e, a, b, c, 7); R0(c, d, e, a, b, 8); R0(b, c, d, e, a, 9); R0(a, b, c, d, e, 10); R0(e, a, b, c, d, 11); R0(d, e, a, b, c, 12); R0(c, d, e, a, b, 13); R0(b, c, d, e, a, 14); R0(a, b, c, d, e, 15); R1(e, a, b, c, d, 16); R1(d, e, a, b, c, 17); R1(c, d, e, a, b, 18); R1(b, c, d, e, a, 19); R2(a, b, c, d, e, 20); R2(e, a, b, c, d, 21); R2(d, e, a, b, c, 22); R2(c, d, e, a, b, 23); R2(b, c, d, e, a, 24); R2(a, b, c, d, e, 25); R2(e, a, b, c, d, 26); R2(d, e, a, b, c, 27); R2(c, d, e, a, b, 28); R2(b, c, d, e, a, 29); R2(a, b, c, d, e, 30); R2(e, a, b, c, d, 31); R2(d, e, a, b, c, 32); R2(c, d, e, a, b, 33); R2(b, c, d, e, a, 34); R2(a, b, c, d, e, 35); R2(e, a, b, c, d, 36); R2(d, e, a, b, c, 37); R2(c, d, e, a, b, 38); R2(b, c, d, e, a, 39); R3(a, b, c, d, e, 40); R3(e, a, b, c, d, 41); R3(d, e, a, b, c, 42); R3(c, d, e, a, b, 43); R3(b, c, d, e, a, 44); R3(a, b, c, d, e, 45); R3(e, a, b, c, d, 46); R3(d, e, a, b, c, 47); R3(c, d, e, a, b, 48); R3(b, c, d, e, a, 49); R3(a, b, c, d, e, 50); R3(e, a, b, c, d, 51); R3(d, e, a, b, c, 52); R3(c, d, e, a, b, 53); R3(b, c, d, e, a, 54); R3(a, b, c, d, e, 55); R3(e, a, b, c, d, 56); R3(d, e, a, b, c, 57); R3(c, d, e, a, b, 58); R3(b, c, d, e, a, 59); R4(a, b, c, d, e, 60); R4(e, a, b, c, d, 61); R4(d, e, a, b, c, 62); R4(c, d, e, a, b, 63); R4(b, c, d, e, a, 64); R4(a, b, c, d, e, 65); R4(e, a, b, c, d, 66); R4(d, e, a, b, c, 67); R4(c, d, e, a, b, 68); R4(b, c, d, e, a, 69); R4(a, b, c, d, e, 70); R4(e, a, b, c, d, 71); R4(d, e, a, b, c, 72); R4(c, d, e, a, b, 73); R4(b, c, d, e, a, 74); R4(a, b, c, d, e, 75); R4(e, a, b, c, d, 76); R4(d, e, a, b, c, 77); R4(c, d, e, a, b, 78); R4(b, c, d, e, a, 79); /* Add the working vars back into context.state[] */ state[0] += a; state[1] += b; state[2] += c; state[3] += d; state[4] += e; } /* SHA1Init - Initialize new context */ SHA_API void SHA1_Init(SHA_CTX *context) { /* SHA1 initialization constants */ context->state[0] = 0x67452301; context->state[1] = 0xEFCDAB89; context->state[2] = 0x98BADCFE; context->state[3] = 0x10325476; context->state[4] = 0xC3D2E1F0; context->count[0] = context->count[1] = 0; } SHA_API void SHA1_Update(SHA_CTX *context, const uint8_t *data, const uint32_t len) { uint32_t i, j; j = context->count[0]; if ((context->count[0] += (len << 3)) < j) { context->count[1]++; } context->count[1] += (len >> 29); j = (j >> 3) & 63; if ((j + len) > 63) { i = 64 - j; memcpy(&context->buffer[j], data, i); SHA1_Transform(context->state, context->buffer); for (; i + 63 < len; i += 64) { SHA1_Transform(context->state, &data[i]); } j = 0; } else { i = 0; } memcpy(&context->buffer[j], &data[i], len - i); } /* Add padding and return the message digest. */ SHA_API void SHA1_Final(unsigned char *digest, SHA_CTX *context) { uint32_t i; uint8_t finalcount[8]; for (i = 0; i < 8; i++) { finalcount[i] = (uint8_t)((context->count[(i >= 4 ? 0 : 1)] >> ((3 - (i & 3)) * 8)) & 255); /* Endian independent */ } SHA1_Update(context, (uint8_t *)"\x80", 1); while ((context->count[0] & 504) != 448) { SHA1_Update(context, (uint8_t *)"\x00", 1); } SHA1_Update(context, finalcount, 8); /* Should cause a SHA1_Transform() */ for (i = 0; i < SHA1_DIGEST_SIZE; i++) { digest[i] = (uint8_t)((context->state[i >> 2] >> ((3 - (i & 3)) * 8)) & 255); } /* Wipe variables */ memset(context, '\0', sizeof(*context)); } /* End of sha1.inl */ webfakes/src/response.h0000644000176200001440000002122314740430263014642 0ustar liggesusers/* response.inl * * Bufferring for HTTP headers for HTTP response. * This function are only intended to be used at the server side. * Optional for HTTP/1.0 and HTTP/1.1, mandatory for HTTP/2. * * This file is part of the CivetWeb project. */ #if defined(NO_RESPONSE_BUFFERING) && defined(USE_HTTP2) #error "HTTP2 works only if NO_RESPONSE_BUFFERING is not set" #endif /* Internal function to free header list */ static void free_buffered_response_header_list(struct mg_connection *conn) { #if !defined(NO_RESPONSE_BUFFERING) while (conn->response_info.num_headers > 0) { conn->response_info.num_headers--; mg_free((void *)conn->response_info .http_headers[conn->response_info.num_headers] .name); conn->response_info.http_headers[conn->response_info.num_headers].name = 0; mg_free((void *)conn->response_info .http_headers[conn->response_info.num_headers] .value); conn->response_info.http_headers[conn->response_info.num_headers] .value = 0; } #else (void)conn; /* Nothing to do */ #endif } /* Send first line of HTTP/1.x response */ static int send_http1_response_status_line(struct mg_connection *conn) { const char *status_txt; const char *http_version = conn->request_info.http_version; int status_code = conn->status_code; if ((status_code < 100) || (status_code > 999)) { /* Set invalid status code to "500 Internal Server Error" */ status_code = 500; } if (!http_version) { http_version = "1.0"; } /* mg_get_response_code_text will never return NULL */ status_txt = mg_get_response_code_text(conn, conn->status_code); if (mg_printf( conn, "HTTP/%s %i %s\r\n", http_version, status_code, status_txt) < 10) { /* Network sending failed */ return 0; } return 1; } /* Initialize a new HTTP response * Parameters: * conn: Current connection handle. * status: HTTP status code (e.g., 200 for "OK"). * Return: * 0: ok * -1: parameter error * -2: invalid connection type * -3: invalid connection status * -4: network error (only if built with NO_RESPONSE_BUFFERING) */ int mg_response_header_start(struct mg_connection *conn, int status) { int ret = 0; if ((conn == NULL) || (status < 100) || (status > 999)) { /* Parameter error */ return -1; } if ((conn->connection_type != CONNECTION_TYPE_REQUEST) || (conn->protocol_type == PROTOCOL_TYPE_WEBSOCKET)) { /* Only allowed in server context */ return -2; } if (conn->request_state != 0) { /* only allowed if nothing was sent up to now */ return -3; } conn->status_code = status; conn->request_state = 1; /* Buffered response is stored, unbuffered response will be sent directly, * but we can only send HTTP/1.x response here */ #if !defined(NO_RESPONSE_BUFFERING) free_buffered_response_header_list(conn); #else if (!send_http1_response_status_line(conn)) { ret = -4; }; conn->request_state = 1; /* Reset from 10 to 1 */ #endif return ret; } /* Add a new HTTP response header line * Parameters: * conn: Current connection handle. * header: Header name. * value: Header value. * value_len: Length of header value, excluding the terminating zero. * Use -1 for "strlen(value)". * Return: * 0: ok * -1: parameter error * -2: invalid connection type * -3: invalid connection status * -4: too many headers * -5: out of memory */ int mg_response_header_add(struct mg_connection *conn, const char *header, const char *value, int value_len) { #if !defined(NO_RESPONSE_BUFFERING) int hidx; #endif if ((conn == NULL) || (header == NULL) || (value == NULL)) { /* Parameter error */ return -1; } if ((conn->connection_type != CONNECTION_TYPE_REQUEST) || (conn->protocol_type == PROTOCOL_TYPE_WEBSOCKET)) { /* Only allowed in server context */ return -2; } if (conn->request_state != 1) { /* only allowed if mg_response_header_start has been called before */ return -3; } #if !defined(NO_RESPONSE_BUFFERING) hidx = conn->response_info.num_headers; if (hidx >= MG_MAX_HEADERS) { /* Too many headers */ return -4; } /* Alloc new element */ conn->response_info.http_headers[hidx].name = mg_strdup_ctx(header, conn->phys_ctx); if (value_len >= 0) { char *hbuf = (char *)mg_malloc_ctx((unsigned)value_len + 1, conn->phys_ctx); if (hbuf) { memcpy(hbuf, value, (unsigned)value_len); hbuf[value_len] = 0; } conn->response_info.http_headers[hidx].value = hbuf; } else { conn->response_info.http_headers[hidx].value = mg_strdup_ctx(value, conn->phys_ctx); } if ((conn->response_info.http_headers[hidx].name == 0) || (conn->response_info.http_headers[hidx].value == 0)) { /* Out of memory */ mg_free((void *)conn->response_info.http_headers[hidx].name); conn->response_info.http_headers[hidx].name = 0; mg_free((void *)conn->response_info.http_headers[hidx].value); conn->response_info.http_headers[hidx].value = 0; return -5; } /* OK, header stored */ conn->response_info.num_headers++; #else if (value_len >= 0) { mg_printf(conn, "%s: %.*s\r\n", header, (int)value_len, value); } else { mg_printf(conn, "%s: %s\r\n", header, value); } conn->request_state = 1; /* Reset from 10 to 1 */ #endif return 0; } /* forward */ static int parse_http_headers(char **buf, struct mg_header hdr[MG_MAX_HEADERS]); /* Add a complete header string (key + value). * Parameters: * conn: Current connection handle. * http1_headers: Header line(s) in the form "name: value". * Return: * >=0: no error, number of header lines added * -1: parameter error * -2: invalid connection type * -3: invalid connection status * -4: too many headers * -5: out of memory */ int mg_response_header_add_lines(struct mg_connection *conn, const char *http1_headers) { struct mg_header add_hdr[MG_MAX_HEADERS]; int num_hdr, i, ret; char *workbuffer, *parse; /* We need to work on a copy of the work buffer, sice parse_http_headers * will modify */ workbuffer = mg_strdup_ctx(http1_headers, conn->phys_ctx); if (!workbuffer) { /* Out of memory */ return -5; } /* Call existing method to split header buffer */ parse = workbuffer; num_hdr = parse_http_headers(&parse, add_hdr); ret = num_hdr; for (i = 0; i < num_hdr; i++) { int lret = mg_response_header_add(conn, add_hdr[i].name, add_hdr[i].value, -1); if ((ret > 0) && (lret < 0)) { /* Store error return value */ ret = lret; } } /* mg_response_header_add created a copy, so we can free the original */ mg_free(workbuffer); return ret; } #if defined(USE_HTTP2) static int http2_send_response_headers(struct mg_connection *conn); #endif /* Send http response * Parameters: * conn: Current connection handle. * Return: * 0: ok * -1: parameter error * -2: invalid connection type * -3: invalid connection status * -4: network send failed */ int mg_response_header_send(struct mg_connection *conn) { #if !defined(NO_RESPONSE_BUFFERING) int i; int has_date = 0; int has_connection = 0; #endif if (conn == NULL) { /* Parameter error */ return -1; } if ((conn->connection_type != CONNECTION_TYPE_REQUEST) || (conn->protocol_type == PROTOCOL_TYPE_WEBSOCKET)) { /* Only allowed in server context */ return -2; } if (conn->request_state != 1) { /* only allowed if mg_response_header_start has been called before */ return -3; } /* State: 2 */ conn->request_state = 2; #if !defined(NO_RESPONSE_BUFFERING) #if defined(USE_HTTP2) if (conn->protocol_type == PROTOCOL_TYPE_HTTP2) { int ret = http2_send_response_headers(conn); free_buffered_response_header_list(conn); return (ret ? 0 : -4); } #endif /* Send */ if (!send_http1_response_status_line(conn)) { free_buffered_response_header_list(conn); return -4; }; for (i = 0; i < conn->response_info.num_headers; i++) { mg_printf(conn, "%s: %s\r\n", conn->response_info.http_headers[i].name, conn->response_info.http_headers[i].value); /* Check for some special headers */ if (!mg_strcasecmp("Date", conn->response_info.http_headers[i].name)) { has_date = 1; } if (!mg_strcasecmp("Connection", conn->response_info.http_headers[i].name)) { has_connection = 1; } } if (!has_date) { time_t curtime = time(NULL); char date[64]; gmt_time_string(date, sizeof(date), &curtime); mg_printf(conn, "Date: %s\r\n", date); } if (!has_connection) { mg_printf(conn, "Connection: %s\r\n", suggest_connection_header(conn)); } #endif mg_write(conn, "\r\n", 2); conn->request_state = 3; /* ok */ free_buffered_response_header_list(conn); return 0; } webfakes/src/civetweb.c0000644000176200001440000225617414740430263014630 0ustar liggesusers/* Copyright (c) 2013-2021 the Civetweb developers * Copyright (c) 2004-2013 Sergey Lyubka * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #if defined(__GNUC__) || defined(__MINGW32__) #ifndef GCC_VERSION #define GCC_VERSION \ (__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__) #endif #if GCC_VERSION >= 40500 /* gcc diagnostic pragmas available */ #define GCC_DIAGNOSTIC #endif #endif #if defined(GCC_DIAGNOSTIC) /* Disable unused macros warnings - not all defines are required * for all systems and all compilers. */ # pragma GCC diagnostic ignored "-Wunused-macros" /* A padding warning is just plain useless */ # pragma GCC diagnostic ignored "-Wpadded" #endif #if defined(__clang__) /* GCC does not (yet) support this pragma */ /* We must set some flags for the headers we include. These flags * are reserved ids according to C99, so we need to disable a * warning for that. */ # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wreserved-id-macro" #endif #if defined(_WIN32) #if !defined(_CRT_SECURE_NO_WARNINGS) #define _CRT_SECURE_NO_WARNINGS /* Disable deprecation warning in VS2005 */ #endif #if !defined(_WIN32_WINNT) /* defined for tdm-gcc so we can use getnameinfo */ #define _WIN32_WINNT 0x0502 #endif #else #if !defined(_GNU_SOURCE) #define _GNU_SOURCE /* for setgroups(), pthread_setname_np() */ #endif #if defined(__linux__) && !defined(_XOPEN_SOURCE) #define _XOPEN_SOURCE 600 /* For flockfile() on Linux */ #endif #if defined(__LSB_VERSION__) || defined(__sun) #define NEED_TIMEGM #define NO_THREAD_NAME #endif #if !defined(_LARGEFILE_SOURCE) #define _LARGEFILE_SOURCE /* For fseeko(), ftello() */ #endif #if !defined(_FILE_OFFSET_BITS) #define _FILE_OFFSET_BITS 64 /* Use 64-bit file offsets by default */ #endif #if !defined(__STDC_FORMAT_MACROS) #define __STDC_FORMAT_MACROS /* wants this for C++ */ #endif #if !defined(__STDC_LIMIT_MACROS) #define __STDC_LIMIT_MACROS /* C++ wants that for INT64_MAX */ #endif #if !defined(_DARWIN_UNLIMITED_SELECT) #define _DARWIN_UNLIMITED_SELECT #endif #if defined(__sun) #define __EXTENSIONS__ /* to expose flockfile and friends in stdio.h */ #define __inline inline /* not recognized on older compiler versions */ #endif #endif #if defined(__clang__) /* Enable reserved-id-macro warning again. */ # pragma GCC diagnostic pop #endif #if defined(USE_LUA) #define USE_TIMERS #endif #if defined(_MSC_VER) /* 'type cast' : conversion from 'int' to 'HANDLE' of greater size */ # pragma warning(disable : 4306) /* conditional expression is constant: introduced by FD_SET(..) */ # pragma warning(disable : 4127) /* non-constant aggregate initializer: issued due to missing C99 support */ # pragma warning(disable : 4204) /* padding added after data member */ # pragma warning(disable : 4820) /* not defined as a preprocessor macro, replacing with '0' for '#if/#elif' */ # pragma warning(disable : 4668) /* no function prototype given: converting '()' to '(void)' */ # pragma warning(disable : 4255) /* function has been selected for automatic inline expansion */ # pragma warning(disable : 4711) #endif /* This code uses static_assert to check some conditions. * Unfortunately some compilers still do not support it, so we have a * replacement function here. */ #if defined(__STDC_VERSION__) && __STDC_VERSION__ > 201100L #define mg_static_assert _Static_assert #elif defined(__cplusplus) && __cplusplus >= 201103L #define mg_static_assert static_assert #else char static_assert_replacement[1]; #define mg_static_assert(cond, txt) \ extern char static_assert_replacement[(cond) ? 1 : -1] #endif mg_static_assert(sizeof(int) == 4 || sizeof(int) == 8, "int data type size check"); mg_static_assert(sizeof(void *) == 4 || sizeof(void *) == 8, "pointer data type size check"); mg_static_assert(sizeof(void *) >= sizeof(int), "data type size check"); /* Select queue implementation. Diagnosis features originally only implemented * for the "ALTERNATIVE_QUEUE" have been ported to the previous queue * implementation (NO_ALTERNATIVE_QUEUE) as well. The new configuration value * "CONNECTION_QUEUE_SIZE" is only available for the previous queue * implementation, since the queue length is independent from the number of * worker threads there, while the new queue is one element per worker thread. * */ #if defined(NO_ALTERNATIVE_QUEUE) && defined(ALTERNATIVE_QUEUE) /* The queues are exclusive or - only one can be used. */ #error \ "Define ALTERNATIVE_QUEUE or NO_ALTERNATIVE_QUEUE (or none of them), but not both" #endif #if !defined(NO_ALTERNATIVE_QUEUE) && !defined(ALTERNATIVE_QUEUE) /* Use a default implementation */ #define NO_ALTERNATIVE_QUEUE #endif #if defined(NO_FILESYSTEMS) && !defined(NO_FILES) /* File system access: * NO_FILES = do not serve any files from the file system automatically. * However, with NO_FILES CivetWeb may still write log files, read access * control files, default error page files or use API functions like * mg_send_file in callbacks to send files from the server local * file system. * NO_FILES only disables the automatic mapping between URLs and local * file names. * NO_FILESYSTEM = do not access any file at all. Useful for embedded * devices without file system. Logging to files in not available * (use callbacks instead) and API functions like mg_send_file are not * available. * If NO_FILESYSTEM is set, NO_FILES must be set as well. */ #error "Inconsistent build flags, NO_FILESYSTEMS requires NO_FILES" #endif /* DTL -- including winsock2.h works better if lean and mean */ #if !defined(WIN32_LEAN_AND_MEAN) #define WIN32_LEAN_AND_MEAN #endif #if defined(__SYMBIAN32__) /* According to https://en.wikipedia.org/wiki/Symbian#History, * Symbian is no longer maintained since 2014-01-01. * Support for Symbian has been removed from CivetWeb */ #error "Symbian is no longer maintained. CivetWeb no longer supports Symbian." #endif /* __SYMBIAN32__ */ #if defined(__ZEPHYR__) #include #include #include #include #include #include #include #include #include #include #include /* Max worker threads is the max of pthreads minus the main application thread * and minus the main civetweb thread, thus -2 */ #define MAX_WORKER_THREADS (CONFIG_MAX_PTHREAD_COUNT - 2) #if defined(USE_STACK_SIZE) && (USE_STACK_SIZE > 1) #define ZEPHYR_STACK_SIZE USE_STACK_SIZE #else #define ZEPHYR_STACK_SIZE (1024 * 16) #endif K_THREAD_STACK_DEFINE(civetweb_main_stack, ZEPHYR_STACK_SIZE); K_THREAD_STACK_ARRAY_DEFINE(civetweb_worker_stacks, MAX_WORKER_THREADS, ZEPHYR_STACK_SIZE); static int zephyr_worker_stack_index; #endif #if !defined(CIVETWEB_HEADER_INCLUDED) /* Include the header file here, so the CivetWeb interface is defined for the * entire implementation, including the following forward definitions. */ #include "civetweb.h" #endif #if !defined(DEBUG_TRACE) #if defined(DEBUG) static void DEBUG_TRACE_FUNC(const char *func, unsigned line, PRINTF_FORMAT_STRING(const char *fmt), ...) PRINTF_ARGS(3, 4); #define DEBUG_TRACE(fmt, ...) \ DEBUG_TRACE_FUNC(__func__, __LINE__, fmt, __VA_ARGS__) #define NEED_DEBUG_TRACE_FUNC #if !defined(DEBUG_TRACE_STREAM) #define DEBUG_TRACE_STREAM stdout #endif #else #define DEBUG_TRACE(fmt, ...) \ do { \ } while (0) #endif /* DEBUG */ #endif /* DEBUG_TRACE */ #if !defined(DEBUG_ASSERT) #if defined(DEBUG) #include #define DEBUG_ASSERT(cond) \ do { \ if (!(cond)) { \ DEBUG_TRACE("ASSERTION FAILED: %s", #cond); \ exit(2); /* Exit with error */ \ } \ } while (0) #else #define DEBUG_ASSERT(cond) #endif /* DEBUG */ #endif #if defined(__GNUC__) && defined(GCC_INSTRUMENTATION) void __cyg_profile_func_enter(void *this_fn, void *call_site) __attribute__((no_instrument_function)); void __cyg_profile_func_exit(void *this_fn, void *call_site) __attribute__((no_instrument_function)); void __cyg_profile_func_enter(void *this_fn, void *call_site) { if ((void *)this_fn != (void *)printf) { /* printf("E %p %p\n", this_fn, call_site); */ } } void __cyg_profile_func_exit(void *this_fn, void *call_site) { if ((void *)this_fn != (void *)printf) { /* printf("X %p %p\n", this_fn, call_site); */ } } #endif #if !defined(IGNORE_UNUSED_RESULT) #define IGNORE_UNUSED_RESULT(a) ((void)((a) && 1)) #endif #if defined(__GNUC__) || defined(__MINGW32__) /* GCC unused function attribute seems fundamentally broken. * Several attempts to tell the compiler "THIS FUNCTION MAY BE USED * OR UNUSED" for individual functions failed. * Either the compiler creates an "unused-function" warning if a * function is not marked with __attribute__((unused)). * On the other hand, if the function is marked with this attribute, * but is used, the compiler raises a completely idiotic * "used-but-marked-unused" warning - and * # pragma GCC diagnostic ignored "-Wused-but-marked-unused" * raises error: unknown option after "# pragma GCC diagnostic". * Disable this warning completely, until the GCC guys sober up * again. */ # pragma GCC diagnostic ignored "-Wunused-function" #define FUNCTION_MAY_BE_UNUSED /* __attribute__((unused)) */ #else #define FUNCTION_MAY_BE_UNUSED #endif /* Some ANSI #includes are not available on Windows CE and Zephyr */ #if !defined(_WIN32_WCE) && !defined(__ZEPHYR__) #include #include #include #include #include #include #endif /* !_WIN32_WCE */ #if defined(__clang__) /* When using -Weverything, clang does not accept it's own headers * in a release build configuration. Disable what is too much in * -Weverything. */ # pragma clang diagnostic ignored "-Wdisabled-macro-expansion" #endif #if defined(__GNUC__) || defined(__MINGW32__) /* Who on earth came to the conclusion, using __DATE__ should rise * an "expansion of date or time macro is not reproducible" * warning. That's exactly what was intended by using this macro. * Just disable this nonsense warning. */ /* And disabling them does not work either: * # pragma clang diagnostic ignored "-Wno-error=date-time" * # pragma clang diagnostic ignored "-Wdate-time" * So we just have to disable ALL warnings for some lines * of code. * This seems to be a known GCC bug, not resolved since 2012: * https://gcc.gnu.org/bugzilla/show_bug.cgi?id=53431 */ #endif #if defined(__MACH__) && defined(__APPLE__) /* Apple OSX section */ #if defined(__clang__) #if (__clang_major__ == 3) && ((__clang_minor__ == 7) || (__clang_minor__ == 8)) /* Avoid warnings for Xcode 7. It seems it does no longer exist in Xcode 8 */ # pragma clang diagnostic ignored "-Wno-reserved-id-macro" # pragma clang diagnostic ignored "-Wno-keyword-macro" #endif #endif #ifndef CLOCK_MONOTONIC #define CLOCK_MONOTONIC (1) #endif #ifndef CLOCK_REALTIME #define CLOCK_REALTIME (2) #endif #include #include #include #include #include /* clock_gettime is not implemented on OSX prior to 10.12 */ static int _civet_clock_gettime(int clk_id, struct timespec *t) { memset(t, 0, sizeof(*t)); if (clk_id == CLOCK_REALTIME) { struct timeval now; int rv = gettimeofday(&now, NULL); if (rv) { return rv; } t->tv_sec = now.tv_sec; t->tv_nsec = now.tv_usec * 1000; return 0; } else if (clk_id == CLOCK_MONOTONIC) { static uint64_t clock_start_time = 0; static mach_timebase_info_data_t timebase_ifo = {0, 0}; uint64_t now = mach_absolute_time(); if (clock_start_time == 0) { kern_return_t mach_status = mach_timebase_info(&timebase_ifo); DEBUG_ASSERT(mach_status == KERN_SUCCESS); /* appease "unused variable" warning for release builds */ (void)mach_status; clock_start_time = now; } now = (uint64_t)((double)(now - clock_start_time) * (double)timebase_ifo.numer / (double)timebase_ifo.denom); t->tv_sec = now / 1000000000; t->tv_nsec = now % 1000000000; return 0; } return -1; /* EINVAL - Clock ID is unknown */ } /* if clock_gettime is declared, then __CLOCK_AVAILABILITY will be defined */ #if defined(__CLOCK_AVAILABILITY) /* If we compiled with Mac OSX 10.12 or later, then clock_gettime will be * declared but it may be NULL at runtime. So we need to check before using * it. */ static int _civet_safe_clock_gettime(int clk_id, struct timespec *t) { if (clock_gettime) { return clock_gettime(clk_id, t); } return _civet_clock_gettime(clk_id, t); } #define clock_gettime _civet_safe_clock_gettime #else #define clock_gettime _civet_clock_gettime #endif #endif #if defined(_WIN32) #define ERROR_TRY_AGAIN(err) ((err) == WSAEWOULDBLOCK) #else /* Unix might return different error codes indicating to try again. * For Linux EAGAIN==EWOULDBLOCK, maybe EAGAIN!=EWOULDBLOCK is history from * decades ago, but better check both and let the compiler optimize it. */ #define ERROR_TRY_AGAIN(err) \ (((err) == EAGAIN) || ((err) == EWOULDBLOCK) || ((err) == EINTR)) #endif #if defined(USE_ZLIB) #include "zconf.h" #include "zlib.h" #endif /********************************************************************/ /* CivetWeb configuration defines */ /********************************************************************/ /* Maximum number of threads that can be configured. * The number of threads actually created depends on the "num_threads" * configuration parameter, but this is the upper limit. */ #if !defined(MAX_WORKER_THREADS) #define MAX_WORKER_THREADS (1024 * 64) /* in threads (count) */ #endif /* Timeout interval for select/poll calls. * The timeouts depend on "*_timeout_ms" configuration values, but long * timeouts are split into timouts as small as SOCKET_TIMEOUT_QUANTUM. * This reduces the time required to stop the server. */ #if !defined(SOCKET_TIMEOUT_QUANTUM) #define SOCKET_TIMEOUT_QUANTUM (2000) /* in ms */ #endif /* Do not try to compress files smaller than this limit. */ #if !defined(MG_FILE_COMPRESSION_SIZE_LIMIT) #define MG_FILE_COMPRESSION_SIZE_LIMIT (1024) /* in bytes */ #endif #if !defined(PASSWORDS_FILE_NAME) #define PASSWORDS_FILE_NAME ".htpasswd" #endif /* Initial buffer size for all CGI environment variables. In case there is * not enough space, another block is allocated. */ #if !defined(CGI_ENVIRONMENT_SIZE) #define CGI_ENVIRONMENT_SIZE (4096) /* in bytes */ #endif /* Maximum number of environment variables. */ #if !defined(MAX_CGI_ENVIR_VARS) #define MAX_CGI_ENVIR_VARS (256) /* in variables (count) */ #endif /* General purpose buffer size. */ #if !defined(MG_BUF_LEN) /* in bytes */ #define MG_BUF_LEN (1024 * 8) #endif /********************************************************************/ /* Helper macros */ #if !defined(ARRAY_SIZE) #define ARRAY_SIZE(array) (sizeof(array) / sizeof(array[0])) #endif #include /* Standard defines */ #if !defined(INT64_MAX) #define INT64_MAX (9223372036854775807) #endif #define SHUTDOWN_RD (0) #define SHUTDOWN_WR (1) #define SHUTDOWN_BOTH (2) mg_static_assert(MAX_WORKER_THREADS >= 1, "worker threads must be a positive number"); mg_static_assert(sizeof(size_t) == 4 || sizeof(size_t) == 8, "size_t data type size check"); #if defined(_WIN32) /* WINDOWS include block */ #include /* *alloc( */ #include /* *alloc( */ #include /* struct timespec */ #include #include /* DTL add for SO_EXCLUSIVE */ #include typedef const char *SOCK_OPT_TYPE; /* For a detailed description of these *_PATH_MAX defines, see * https://github.com/civetweb/civetweb/issues/937. */ /* UTF8_PATH_MAX is a char buffer size for 259 BMP characters in UTF-8 plus * null termination, rounded up to the next 4 bytes boundary */ #define UTF8_PATH_MAX (3 * 260) /* UTF16_PATH_MAX is the 16-bit wchar_t buffer size required for 259 BMP * characters plus termination. (Note: wchar_t is 16 bit on Windows) */ #define UTF16_PATH_MAX (260) #if !defined(_IN_PORT_T) #if !defined(in_port_t) #define in_port_t u_short #endif #endif #if defined(_WIN32_WCE) #error "WinCE support has ended" #endif #include #include #include #define MAKEUQUAD(lo, hi) \ ((uint64_t)(((uint32_t)(lo)) | ((uint64_t)((uint32_t)(hi))) << 32)) #define RATE_DIFF (10000000) /* 100 nsecs */ #define EPOCH_DIFF (MAKEUQUAD(0xd53e8000, 0x019db1de)) #define SYS2UNIX_TIME(lo, hi) \ ((time_t)((MAKEUQUAD((lo), (hi)) - EPOCH_DIFF) / RATE_DIFF)) /* Visual Studio 6 does not know __func__ or __FUNCTION__ * The rest of MS compilers use __FUNCTION__, not C99 __func__ * Also use _strtoui64 on modern M$ compilers */ #if defined(_MSC_VER) #if (_MSC_VER < 1300) #define STRX(x) #x #define STR(x) STRX(x) #define __func__ __FILE__ ":" STR(__LINE__) #define strtoull(x, y, z) ((unsigned __int64)_atoi64(x)) #define strtoll(x, y, z) (_atoi64(x)) #else #define __func__ __FUNCTION__ #define strtoull(x, y, z) (_strtoui64(x, y, z)) #define strtoll(x, y, z) (_strtoi64(x, y, z)) #endif #endif /* _MSC_VER */ #define ERRNO ((int)(GetLastError())) #define NO_SOCKLEN_T #if defined(_WIN64) || defined(__MINGW64__) #if !defined(SSL_LIB) #if defined(OPENSSL_API_3_0) #define SSL_LIB "libssl-3-x64.dll" #define CRYPTO_LIB "libcrypto-3-x64.dll" #endif #if defined(OPENSSL_API_1_1) #define SSL_LIB "libssl-1_1-x64.dll" #define CRYPTO_LIB "libcrypto-1_1-x64.dll" #endif /* OPENSSL_API_1_1 */ #if defined(OPENSSL_API_1_0) #define SSL_LIB "ssleay64.dll" #define CRYPTO_LIB "libeay64.dll" #endif /* OPENSSL_API_1_0 */ #endif #else /* defined(_WIN64) || defined(__MINGW64__) */ #if !defined(SSL_LIB) #if defined(OPENSSL_API_3_0) #define SSL_LIB "libssl-3.dll" #define CRYPTO_LIB "libcrypto-3.dll" #endif #if defined(OPENSSL_API_1_1) #define SSL_LIB "libssl-1_1.dll" #define CRYPTO_LIB "libcrypto-1_1.dll" #endif /* OPENSSL_API_1_1 */ #if defined(OPENSSL_API_1_0) #define SSL_LIB "ssleay32.dll" #define CRYPTO_LIB "libeay32.dll" #endif /* OPENSSL_API_1_0 */ #endif /* SSL_LIB */ #endif /* defined(_WIN64) || defined(__MINGW64__) */ #define O_NONBLOCK (0) #if !defined(W_OK) #define W_OK (2) /* http://msdn.microsoft.com/en-us/library/1w06ktdy.aspx */ #endif #define _POSIX_ #include #define INT64_FMT PRId64 #define UINT64_FMT PRIu64 #define WINCDECL __cdecl #define vsnprintf_impl _vsnprintf #define access _access #define mg_sleep(x) (Sleep(x)) #define pipe(x) _pipe(x, MG_BUF_LEN, _O_BINARY) #if !defined(popen) #define popen(x, y) (_popen(x, y)) #endif #if !defined(pclose) #define pclose(x) (_pclose(x)) #endif #define close(x) (_close(x)) #define dlsym(x, y) (GetProcAddress((HINSTANCE)(x), (y))) #define RTLD_LAZY (0) #define fseeko(x, y, z) ((_lseeki64(_fileno(x), (y), (z)) == -1) ? -1 : 0) #define fdopen(x, y) (_fdopen((x), (y))) #define write(x, y, z) (_write((x), (y), (unsigned)z)) #define read(x, y, z) (_read((x), (y), (unsigned)z)) #define flockfile(x) ((void)pthread_mutex_lock(&global_log_file_lock)) #define funlockfile(x) ((void)pthread_mutex_unlock(&global_log_file_lock)) #define sleep(x) (Sleep((x)*1000)) #define rmdir(x) (_rmdir(x)) #if defined(_WIN64) || !defined(__MINGW32__) /* Only MinGW 32 bit is missing this function */ #define timegm(x) (_mkgmtime(x)) #else time_t timegm(struct tm *tm); #define NEED_TIMEGM #endif #if !defined(fileno) #define fileno(x) (_fileno(x)) #endif /* !fileno MINGW #defines fileno */ typedef struct { CRITICAL_SECTION sec; /* Immovable */ } pthread_mutex_t; typedef DWORD pthread_key_t; typedef HANDLE pthread_t; typedef struct { pthread_mutex_t threadIdSec; struct mg_workerTLS *waiting_thread; /* The chain of threads */ } pthread_cond_t; #if !defined(__clockid_t_defined) typedef DWORD clockid_t; #endif #if !defined(CLOCK_MONOTONIC) #define CLOCK_MONOTONIC (1) #endif #if !defined(CLOCK_REALTIME) #define CLOCK_REALTIME (2) #endif #if !defined(CLOCK_THREAD) #define CLOCK_THREAD (3) #endif #if !defined(CLOCK_PROCESS) #define CLOCK_PROCESS (4) #endif #if defined(_MSC_VER) && (_MSC_VER >= 1900) #define _TIMESPEC_DEFINED #endif #if !defined(_TIMESPEC_DEFINED) struct timespec { time_t tv_sec; /* seconds */ long tv_nsec; /* nanoseconds */ }; #endif #if !defined(WIN_PTHREADS_TIME_H) #define MUST_IMPLEMENT_CLOCK_GETTIME #endif #if defined(MUST_IMPLEMENT_CLOCK_GETTIME) #define clock_gettime mg_clock_gettime static int clock_gettime(clockid_t clk_id, struct timespec *tp) { FILETIME ft; ULARGE_INTEGER li, li2; BOOL ok = FALSE; double d; static double perfcnt_per_sec = 0.0; static BOOL initialized = FALSE; if (!initialized) { QueryPerformanceFrequency((LARGE_INTEGER *)&li); perfcnt_per_sec = 1.0 / li.QuadPart; initialized = TRUE; } if (tp) { memset(tp, 0, sizeof(*tp)); if (clk_id == CLOCK_REALTIME) { /* BEGIN: CLOCK_REALTIME = wall clock (date and time) */ GetSystemTimeAsFileTime(&ft); li.LowPart = ft.dwLowDateTime; li.HighPart = ft.dwHighDateTime; li.QuadPart -= 116444736000000000; /* 1.1.1970 in filedate */ tp->tv_sec = (time_t)(li.QuadPart / 10000000); tp->tv_nsec = (long)(li.QuadPart % 10000000) * 100; ok = TRUE; /* END: CLOCK_REALTIME */ } else if (clk_id == CLOCK_MONOTONIC) { /* BEGIN: CLOCK_MONOTONIC = stopwatch (time differences) */ QueryPerformanceCounter((LARGE_INTEGER *)&li); d = li.QuadPart * perfcnt_per_sec; tp->tv_sec = (time_t)d; d -= (double)tp->tv_sec; tp->tv_nsec = (long)(d * 1.0E9); ok = TRUE; /* END: CLOCK_MONOTONIC */ } else if (clk_id == CLOCK_THREAD) { /* BEGIN: CLOCK_THREAD = CPU usage of thread */ FILETIME t_create, t_exit, t_kernel, t_user; if (GetThreadTimes(GetCurrentThread(), &t_create, &t_exit, &t_kernel, &t_user)) { li.LowPart = t_user.dwLowDateTime; li.HighPart = t_user.dwHighDateTime; li2.LowPart = t_kernel.dwLowDateTime; li2.HighPart = t_kernel.dwHighDateTime; li.QuadPart += li2.QuadPart; tp->tv_sec = (time_t)(li.QuadPart / 10000000); tp->tv_nsec = (long)(li.QuadPart % 10000000) * 100; ok = TRUE; } /* END: CLOCK_THREAD */ } else if (clk_id == CLOCK_PROCESS) { /* BEGIN: CLOCK_PROCESS = CPU usage of process */ FILETIME t_create, t_exit, t_kernel, t_user; if (GetProcessTimes(GetCurrentProcess(), &t_create, &t_exit, &t_kernel, &t_user)) { li.LowPart = t_user.dwLowDateTime; li.HighPart = t_user.dwHighDateTime; li2.LowPart = t_kernel.dwLowDateTime; li2.HighPart = t_kernel.dwHighDateTime; li.QuadPart += li2.QuadPart; tp->tv_sec = (time_t)(li.QuadPart / 10000000); tp->tv_nsec = (long)(li.QuadPart % 10000000) * 100; ok = TRUE; } /* END: CLOCK_PROCESS */ } else { /* BEGIN: unknown clock */ /* ok = FALSE; already set by init */ /* END: unknown clock */ } } return ok ? 0 : -1; } #endif #define pid_t HANDLE /* MINGW typedefs pid_t to int. Using #define here. */ static int pthread_mutex_lock(pthread_mutex_t *); static int pthread_mutex_unlock(pthread_mutex_t *); static void path_to_unicode(const struct mg_connection *conn, const char *path, wchar_t *wbuf, size_t wbuf_len); /* All file operations need to be rewritten to solve #246. */ struct mg_file; static const char *mg_fgets(char *buf, size_t size, struct mg_file *filep); /* POSIX dirent interface */ struct dirent { char d_name[UTF8_PATH_MAX]; }; typedef struct DIR { HANDLE handle; WIN32_FIND_DATAW info; struct dirent result; } DIR; #if defined(HAVE_POLL) #define mg_pollfd pollfd #else struct mg_pollfd { SOCKET fd; short events; short revents; }; #endif /* Mark required libraries */ #if defined(_MSC_VER) # pragma comment(lib, "Ws2_32.lib") #endif #else /* defined(_WIN32) - WINDOWS vs UNIX include block */ #include /* Linux & co. internally use UTF8 */ #define UTF8_PATH_MAX (PATH_MAX) typedef const void *SOCK_OPT_TYPE; #if defined(ANDROID) typedef unsigned short int in_port_t; #endif #if !defined(__ZEPHYR__) #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(USE_X_DOM_SOCKET) #include #endif #endif #define vsnprintf_impl vsnprintf #if !defined(NO_SSL_DL) && !defined(NO_SSL) #include #endif #if defined(__MACH__) && defined(__APPLE__) #define SSL_LIB "libssl.dylib" #define CRYPTO_LIB "libcrypto.dylib" #else #if !defined(SSL_LIB) #define SSL_LIB "libssl.so" #endif #if !defined(CRYPTO_LIB) #define CRYPTO_LIB "libcrypto.so" #endif #endif #if !defined(O_BINARY) #define O_BINARY (0) #endif /* O_BINARY */ #define closesocket(a) (close(a)) #define mg_mkdir(conn, path, mode) (mkdir(path, mode)) #define mg_remove(conn, x) (remove(x)) #define mg_sleep(x) (usleep((x)*1000)) #define mg_opendir(conn, x) (opendir(x)) #define mg_closedir(x) (closedir(x)) #define mg_readdir(x) (readdir(x)) #define ERRNO (errno) #define INVALID_SOCKET (-1) #define INT64_FMT PRId64 #define UINT64_FMT PRIu64 typedef int SOCKET; #define WINCDECL #if defined(__hpux) /* HPUX 11 does not have monotonic, fall back to realtime */ #if !defined(CLOCK_MONOTONIC) #define CLOCK_MONOTONIC CLOCK_REALTIME #endif /* HPUX defines socklen_t incorrectly as size_t which is 64bit on * Itanium. Without defining _XOPEN_SOURCE or _XOPEN_SOURCE_EXTENDED * the prototypes use int* rather than socklen_t* which matches the * actual library expectation. When called with the wrong size arg * accept() returns a zero client inet addr and check_acl() always * fails. Since socklen_t is widely used below, just force replace * their typedef with int. - DTL */ #define socklen_t int #endif /* hpux */ #define mg_pollfd pollfd #endif /* defined(_WIN32) - WINDOWS vs UNIX include block */ /* In case our C library is missing "timegm", provide an implementation */ #if defined(NEED_TIMEGM) static inline int is_leap(int y) { return (y % 4 == 0 && y % 100 != 0) || y % 400 == 0; } static inline int count_leap(int y) { return (y - 1969) / 4 - (y - 1901) / 100 + (y - 1601) / 400; } time_t timegm(struct tm *tm) { static const unsigned short ydays[] = { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365}; int year = tm->tm_year + 1900; int mon = tm->tm_mon; int mday = tm->tm_mday - 1; int hour = tm->tm_hour; int min = tm->tm_min; int sec = tm->tm_sec; if (year < 1970 || mon < 0 || mon > 11 || mday < 0 || (mday >= ydays[mon + 1] - ydays[mon] + (mon == 1 && is_leap(year) ? 1 : 0)) || hour < 0 || hour > 23 || min < 0 || min > 59 || sec < 0 || sec > 60) return -1; time_t res = year - 1970; res *= 365; res += mday; res += ydays[mon] + (mon > 1 && is_leap(year) ? 1 : 0); res += count_leap(year); res *= 24; res += hour; res *= 60; res += min; res *= 60; res += sec; return res; } #endif /* NEED_TIMEGM */ /* va_copy should always be a macro, C99 and C++11 - DTL */ #if !defined(va_copy) #define va_copy(x, y) ((x) = (y)) #endif #if defined(_WIN32) /* Create substitutes for POSIX functions in Win32. */ #if defined(GCC_DIAGNOSTIC) /* Show no warning in case system functions are not used. */ # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wunused-function" #endif static pthread_mutex_t global_log_file_lock; FUNCTION_MAY_BE_UNUSED static DWORD pthread_self(void) { return GetCurrentThreadId(); } FUNCTION_MAY_BE_UNUSED static int pthread_key_create( pthread_key_t *key, void (*_ignored)(void *) /* destructor not supported for Windows */ ) { (void)_ignored; if ((key != 0)) { *key = TlsAlloc(); return (*key != TLS_OUT_OF_INDEXES) ? 0 : -1; } return -2; } FUNCTION_MAY_BE_UNUSED static int pthread_key_delete(pthread_key_t key) { return TlsFree(key) ? 0 : 1; } FUNCTION_MAY_BE_UNUSED static int pthread_setspecific(pthread_key_t key, void *value) { return TlsSetValue(key, value) ? 0 : 1; } FUNCTION_MAY_BE_UNUSED static void * pthread_getspecific(pthread_key_t key) { return TlsGetValue(key); } #if defined(GCC_DIAGNOSTIC) /* Enable unused function warning again */ # pragma GCC diagnostic pop #endif static struct pthread_mutex_undefined_struct *pthread_mutex_attr = NULL; #else static pthread_mutexattr_t pthread_mutex_attr; #endif /* _WIN32 */ #if defined(GCC_DIAGNOSTIC) /* Show no warning in case system functions are not used. */ # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wunused-function" #endif /* defined(GCC_DIAGNOSTIC) */ #if defined(__clang__) /* Show no warning in case system functions are not used. */ # pragma clang diagnostic push # pragma clang diagnostic ignored "-Wunused-function" #endif static pthread_mutex_t global_lock_mutex; FUNCTION_MAY_BE_UNUSED static void mg_global_lock(void) { (void)pthread_mutex_lock(&global_lock_mutex); } FUNCTION_MAY_BE_UNUSED static void mg_global_unlock(void) { (void)pthread_mutex_unlock(&global_lock_mutex); } #if defined(_WIN64) mg_static_assert(SIZE_MAX == 0xFFFFFFFFFFFFFFFFu, "Mismatch for atomic types"); #elif defined(_WIN32) mg_static_assert(SIZE_MAX == 0xFFFFFFFFu, "Mismatch for atomic types"); #endif /* Atomic functions working on ptrdiff_t ("signed size_t"). * Operations: Increment, Decrement, Add, Maximum. * Up to size_t, they do not an atomic "load" operation. */ FUNCTION_MAY_BE_UNUSED static ptrdiff_t mg_atomic_inc(volatile ptrdiff_t *addr) { ptrdiff_t ret; #if defined(_WIN64) && defined(_WIN32) && !defined(NO_ATOMICS) ret = InterlockedIncrement64(addr); #elif defined(__GNUC__) \ && ((__GNUC__ > 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ > 0))) \ && !defined(NO_ATOMICS) ret = __sync_add_and_fetch(addr, 1); #elif defined(_WIN32) && !defined(NO_ATOMICS) ret = InterlockedIncrement(addr); #else mg_global_lock(); ret = (++(*addr)); mg_global_unlock(); #endif return ret; } FUNCTION_MAY_BE_UNUSED static ptrdiff_t mg_atomic_dec(volatile ptrdiff_t *addr) { ptrdiff_t ret; #if defined(_WIN64) && defined(_WIN32) && !defined(NO_ATOMICS) ret = InterlockedDecrement64(addr); #elif defined(__GNUC__) \ && ((__GNUC__ > 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ > 0))) \ && !defined(NO_ATOMICS) ret = __sync_sub_and_fetch(addr, 1); #elif defined(_WIN32) && !defined(NO_ATOMICS) ret = InterlockedDecrement(addr); #else mg_global_lock(); ret = (--(*addr)); mg_global_unlock(); #endif return ret; } #if defined(USE_SERVER_STATS) || defined(STOP_FLAG_NEEDS_LOCK) static ptrdiff_t mg_atomic_add(volatile ptrdiff_t *addr, ptrdiff_t value) { ptrdiff_t ret; #if defined(_WIN64) && !defined(NO_ATOMICS) ret = InterlockedAdd64(addr, value); #elif defined(_WIN32) && !defined(NO_ATOMICS) ret = InterlockedExchangeAdd(addr, value) + value; #elif defined(__GNUC__) \ && ((__GNUC__ > 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ > 0))) \ && !defined(NO_ATOMICS) ret = __sync_add_and_fetch(addr, value); #else mg_global_lock(); *addr += value; ret = (*addr); mg_global_unlock(); #endif return ret; } FUNCTION_MAY_BE_UNUSED static ptrdiff_t mg_atomic_compare_and_swap(volatile ptrdiff_t *addr, ptrdiff_t oldval, ptrdiff_t newval) { ptrdiff_t ret; #if defined(_WIN64) && !defined(NO_ATOMICS) ret = InterlockedCompareExchange64(addr, newval, oldval); #elif defined(_WIN32) && !defined(NO_ATOMICS) ret = InterlockedCompareExchange(addr, newval, oldval); #elif defined(__GNUC__) \ && ((__GNUC__ > 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ > 0))) \ && !defined(NO_ATOMICS) ret = __sync_val_compare_and_swap(addr, oldval, newval); #else mg_global_lock(); ret = *addr; if ((ret != newval) && (ret == oldval)) { *addr = newval; } mg_global_unlock(); #endif return ret; } static void mg_atomic_max(volatile ptrdiff_t *addr, ptrdiff_t value) { register ptrdiff_t tmp = *addr; #if defined(_WIN64) && !defined(NO_ATOMICS) while (tmp < value) { tmp = InterlockedCompareExchange64(addr, value, tmp); } #elif defined(_WIN32) && !defined(NO_ATOMICS) while (tmp < value) { tmp = InterlockedCompareExchange(addr, value, tmp); } #elif defined(__GNUC__) \ && ((__GNUC__ > 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ > 0))) \ && !defined(NO_ATOMICS) while (tmp < value) { tmp = __sync_val_compare_and_swap(addr, tmp, value); } #else mg_global_lock(); if (*addr < value) { *addr = value; } mg_global_unlock(); #endif } static int64_t mg_atomic_add64(volatile int64_t *addr, int64_t value) { int64_t ret; #if defined(_WIN64) && !defined(NO_ATOMICS) ret = InterlockedAdd64(addr, value); #elif defined(_WIN32) && !defined(NO_ATOMICS) ret = InterlockedExchangeAdd64(addr, value) + value; #elif defined(__GNUC__) \ && ((__GNUC__ > 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ > 0))) \ && !defined(NO_ATOMICS) ret = __sync_add_and_fetch(addr, value); #else mg_global_lock(); *addr += value; ret = (*addr); mg_global_unlock(); #endif return ret; } #endif #if defined(GCC_DIAGNOSTIC) /* Show no warning in case system functions are not used. */ # pragma GCC diagnostic pop #endif /* defined(GCC_DIAGNOSTIC) */ #if defined(__clang__) /* Show no warning in case system functions are not used. */ # pragma clang diagnostic pop #endif #if defined(USE_SERVER_STATS) struct mg_memory_stat { volatile ptrdiff_t totalMemUsed; volatile ptrdiff_t maxMemUsed; volatile ptrdiff_t blockCount; }; static struct mg_memory_stat *get_memory_stat(struct mg_context *ctx); static void * mg_malloc_ex(size_t size, struct mg_context *ctx, const char *file, unsigned line) { void *data = malloc(size + 2 * sizeof(uintptr_t)); void *memory = 0; struct mg_memory_stat *mstat = get_memory_stat(ctx); #if defined(MEMORY_DEBUGGING) char mallocStr[256]; #else (void)file; (void)line; #endif if (data) { ptrdiff_t mmem = mg_atomic_add(&mstat->totalMemUsed, (ptrdiff_t)size); mg_atomic_max(&mstat->maxMemUsed, mmem); mg_atomic_inc(&mstat->blockCount); ((uintptr_t *)data)[0] = size; ((uintptr_t *)data)[1] = (uintptr_t)mstat; memory = (void *)(((char *)data) + 2 * sizeof(uintptr_t)); } #if defined(MEMORY_DEBUGGING) sprintf(mallocStr, "MEM: %p %5lu alloc %7lu %4lu --- %s:%u\n", memory, (unsigned long)size, (unsigned long)mstat->totalMemUsed, (unsigned long)mstat->blockCount, file, line); DEBUG_TRACE("%s", mallocStr); #endif return memory; } static void * mg_calloc_ex(size_t count, size_t size, struct mg_context *ctx, const char *file, unsigned line) { void *data = mg_malloc_ex(size * count, ctx, file, line); if (data) { memset(data, 0, size * count); } return data; } static void mg_free_ex(void *memory, const char *file, unsigned line) { #if defined(MEMORY_DEBUGGING) char mallocStr[256]; #else (void)file; (void)line; #endif if (memory) { void *data = (void *)(((char *)memory) - 2 * sizeof(uintptr_t)); uintptr_t size = ((uintptr_t *)data)[0]; struct mg_memory_stat *mstat = (struct mg_memory_stat *)(((uintptr_t *)data)[1]); mg_atomic_add(&mstat->totalMemUsed, -(ptrdiff_t)size); mg_atomic_dec(&mstat->blockCount); #if defined(MEMORY_DEBUGGING) sprintf(mallocStr, "MEM: %p %5lu free %7lu %4lu --- %s:%u\n", memory, (unsigned long)size, (unsigned long)mstat->totalMemUsed, (unsigned long)mstat->blockCount, file, line); DEBUG_TRACE("%s", mallocStr); #endif free(data); } } static void * mg_realloc_ex(void *memory, size_t newsize, struct mg_context *ctx, const char *file, unsigned line) { void *data; void *_realloc; uintptr_t oldsize; #if defined(MEMORY_DEBUGGING) char mallocStr[256]; #else (void)file; (void)line; #endif if (newsize) { if (memory) { /* Reallocate existing block */ struct mg_memory_stat *mstat; data = (void *)(((char *)memory) - 2 * sizeof(uintptr_t)); oldsize = ((uintptr_t *)data)[0]; mstat = (struct mg_memory_stat *)((uintptr_t *)data)[1]; _realloc = realloc(data, newsize + 2 * sizeof(uintptr_t)); if (_realloc) { data = _realloc; mg_atomic_add(&mstat->totalMemUsed, -(ptrdiff_t)oldsize); #if defined(MEMORY_DEBUGGING) sprintf(mallocStr, "MEM: %p %5lu r-free %7lu %4lu --- %s:%u\n", memory, (unsigned long)oldsize, (unsigned long)mstat->totalMemUsed, (unsigned long)mstat->blockCount, file, line); DEBUG_TRACE("%s", mallocStr); #endif mg_atomic_add(&mstat->totalMemUsed, (ptrdiff_t)newsize); #if defined(MEMORY_DEBUGGING) sprintf(mallocStr, "MEM: %p %5lu r-alloc %7lu %4lu --- %s:%u\n", memory, (unsigned long)newsize, (unsigned long)mstat->totalMemUsed, (unsigned long)mstat->blockCount, file, line); DEBUG_TRACE("%s", mallocStr); #endif *(uintptr_t *)data = newsize; data = (void *)(((char *)data) + 2 * sizeof(uintptr_t)); } else { #if defined(MEMORY_DEBUGGING) DEBUG_TRACE("%s", "MEM: realloc failed\n"); #endif return _realloc; } } else { /* Allocate new block */ data = mg_malloc_ex(newsize, ctx, file, line); } } else { /* Free existing block */ data = 0; mg_free_ex(memory, file, line); } return data; } #define mg_malloc(a) mg_malloc_ex(a, NULL, __FILE__, __LINE__) #define mg_calloc(a, b) mg_calloc_ex(a, b, NULL, __FILE__, __LINE__) #define mg_realloc(a, b) mg_realloc_ex(a, b, NULL, __FILE__, __LINE__) #define mg_free(a) mg_free_ex(a, __FILE__, __LINE__) #define mg_malloc_ctx(a, c) mg_malloc_ex(a, c, __FILE__, __LINE__) #define mg_calloc_ctx(a, b, c) mg_calloc_ex(a, b, c, __FILE__, __LINE__) #define mg_realloc_ctx(a, b, c) mg_realloc_ex(a, b, c, __FILE__, __LINE__) #else /* USE_SERVER_STATS */ static __inline void * mg_malloc(size_t a) { return malloc(a); } static __inline void * mg_calloc(size_t a, size_t b) { return calloc(a, b); } static __inline void * mg_realloc(void *a, size_t b) { return realloc(a, b); } static __inline void mg_free(void *a) { free(a); } #define mg_malloc_ctx(a, c) mg_malloc(a) #define mg_calloc_ctx(a, b, c) mg_calloc(a, b) #define mg_realloc_ctx(a, b, c) mg_realloc(a, b) #define mg_free_ctx(a, c) mg_free(a) #endif /* USE_SERVER_STATS */ static void mg_vsnprintf(const struct mg_connection *conn, int *truncated, char *buf, size_t buflen, const char *fmt, va_list ap); static void mg_snprintf(const struct mg_connection *conn, int *truncated, char *buf, size_t buflen, PRINTF_FORMAT_STRING(const char *fmt), ...) PRINTF_ARGS(5, 6); /* This following lines are just meant as a reminder to use the mg-functions * for memory management */ #if defined(malloc) #undef malloc #endif #if defined(calloc) #undef calloc #endif #if defined(realloc) #undef realloc #endif #if defined(free) #undef free #endif /* #if defined(snprintf) */ /* #undef snprintf */ /* #endif */ #if defined(vsnprintf) #undef vsnprintf #endif #define malloc DO_NOT_USE_THIS_FUNCTION__USE_mg_malloc #define calloc DO_NOT_USE_THIS_FUNCTION__USE_mg_calloc #define realloc DO_NOT_USE_THIS_FUNCTION__USE_mg_realloc #define free DO_NOT_USE_THIS_FUNCTION__USE_mg_free /* #define snprintf DO_NOT_USE_THIS_FUNCTION__USE_mg_snprintf */ #if defined(_WIN32) /* vsnprintf must not be used in any system, * but this define only works well for Windows. */ #define vsnprintf DO_NOT_USE_THIS_FUNCTION__USE_mg_vsnprintf #endif /* mg_init_library counter */ static int mg_init_library_called = 0; #if !defined(NO_SSL) #if defined(OPENSSL_API_1_0) || defined(OPENSSL_API_1_1) \ || defined(OPENSSL_API_3_0) static int mg_openssl_initialized = 0; #endif #if !defined(OPENSSL_API_1_0) && !defined(OPENSSL_API_1_1) \ && !defined(OPENSSL_API_3_0) && !defined(USE_MBEDTLS) #error "Please define OPENSSL_API_#_# or USE_MBEDTLS" #endif #if defined(OPENSSL_API_1_0) && defined(OPENSSL_API_1_1) #error "Multiple OPENSSL_API versions defined" #endif #if defined(OPENSSL_API_1_1) && defined(OPENSSL_API_3_0) #error "Multiple OPENSSL_API versions defined" #endif #if defined(OPENSSL_API_1_0) && defined(OPENSSL_API_3_0) #error "Multiple OPENSSL_API versions defined" #endif #if (defined(OPENSSL_API_1_0) || defined(OPENSSL_API_1_1) \ || defined(OPENSSL_API_3_0)) \ && defined(USE_MBEDTLS) #error "Multiple SSL libraries defined" #endif #endif static pthread_key_t sTlsKey; /* Thread local storage index */ static volatile ptrdiff_t thread_idx_max = 0; #if defined(MG_LEGACY_INTERFACE) #define MG_ALLOW_USING_GET_REQUEST_INFO_FOR_RESPONSE #endif struct mg_workerTLS { int is_master; unsigned long thread_idx; void *user_ptr; #if defined(_WIN32) HANDLE pthread_cond_helper_mutex; struct mg_workerTLS *next_waiting_thread; #endif const char *alpn_proto; #if defined(MG_ALLOW_USING_GET_REQUEST_INFO_FOR_RESPONSE) char txtbuf[4]; #endif }; #if defined(GCC_DIAGNOSTIC) /* Show no warning in case system functions are not used. */ # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wunused-function" #endif /* defined(GCC_DIAGNOSTIC) */ #if defined(__clang__) /* Show no warning in case system functions are not used. */ # pragma clang diagnostic push # pragma clang diagnostic ignored "-Wunused-function" #endif /* Get a unique thread ID as unsigned long, independent from the data type * of thread IDs defined by the operating system API. * If two calls to mg_current_thread_id return the same value, they calls * are done from the same thread. If they return different values, they are * done from different threads. (Provided this function is used in the same * process context and threads are not repeatedly created and deleted, but * CivetWeb does not do that). * This function must match the signature required for SSL id callbacks: * CRYPTO_set_id_callback */ FUNCTION_MAY_BE_UNUSED static unsigned long mg_current_thread_id(void) { #if defined(_WIN32) return GetCurrentThreadId(); #else #if defined(__clang__) # pragma clang diagnostic push # pragma clang diagnostic ignored "-Wunreachable-code" /* For every compiler, either "sizeof(pthread_t) > sizeof(unsigned long)" * or not, so one of the two conditions will be unreachable by construction. * Unfortunately the C standard does not define a way to check this at * compile time, since the #if preprocessor conditions can not use the * sizeof operator as an argument. */ #endif if (sizeof(pthread_t) > sizeof(unsigned long)) { /* This is the problematic case for CRYPTO_set_id_callback: * The OS pthread_t can not be cast to unsigned long. */ struct mg_workerTLS *tls = (struct mg_workerTLS *)pthread_getspecific(sTlsKey); if (tls == NULL) { /* SSL called from an unknown thread: Create some thread index. */ tls = (struct mg_workerTLS *)mg_malloc(sizeof(struct mg_workerTLS)); tls->is_master = -2; /* -2 means "3rd party thread" */ tls->thread_idx = (unsigned)mg_atomic_inc(&thread_idx_max); pthread_setspecific(sTlsKey, tls); } return tls->thread_idx; } else { /* pthread_t may be any data type, so a simple cast to unsigned long * can rise a warning/error, depending on the platform. * Here memcpy is used as an anything-to-anything cast. */ unsigned long ret = 0; pthread_t t = pthread_self(); memcpy(&ret, &t, sizeof(pthread_t)); return ret; } #if defined(__clang__) # pragma clang diagnostic pop #endif #endif } FUNCTION_MAY_BE_UNUSED static uint64_t mg_get_current_time_ns(void) { struct timespec tsnow; clock_gettime(CLOCK_REALTIME, &tsnow); return (((uint64_t)tsnow.tv_sec) * 1000000000) + (uint64_t)tsnow.tv_nsec; } #if defined(GCC_DIAGNOSTIC) /* Show no warning in case system functions are not used. */ # pragma GCC diagnostic pop #endif /* defined(GCC_DIAGNOSTIC) */ #if defined(__clang__) /* Show no warning in case system functions are not used. */ # pragma clang diagnostic pop #endif #if defined(NEED_DEBUG_TRACE_FUNC) static void DEBUG_TRACE_FUNC(const char *func, unsigned line, const char *fmt, ...) { va_list args; struct timespec tsnow; /* Get some operating system independent thread id */ unsigned long thread_id = mg_current_thread_id(); clock_gettime(CLOCK_REALTIME, &tsnow); flockfile(DEBUG_TRACE_STREAM); fprintf(DEBUG_TRACE_STREAM, "*** %lu.%09lu %lu %s:%u: ", (unsigned long)tsnow.tv_sec, (unsigned long)tsnow.tv_nsec, thread_id, func, line); va_start(args, fmt); vfprintf(DEBUG_TRACE_STREAM, fmt, args); va_end(args); putc('\n', DEBUG_TRACE_STREAM); fflush(DEBUG_TRACE_STREAM); funlockfile(DEBUG_TRACE_STREAM); } #endif /* NEED_DEBUG_TRACE_FUNC */ #define MD5_STATIC static #include "md5.h" /* Darwin prior to 7.0 and Win32 do not have socklen_t */ #if defined(NO_SOCKLEN_T) typedef int socklen_t; #endif /* NO_SOCKLEN_T */ #define IP_ADDR_STR_LEN (50) /* IPv6 hex string is 46 chars */ #if !defined(MSG_NOSIGNAL) #define MSG_NOSIGNAL (0) #endif /* SSL: mbedTLS vs. no-ssl vs. OpenSSL */ #if defined(USE_MBEDTLS) /* mbedTLS */ #include "mod_mbedtls.h" #elif defined(NO_SSL) /* no SSL */ typedef struct SSL SSL; /* dummy for SSL argument to push/pull */ typedef struct SSL_CTX SSL_CTX; #elif defined(NO_SSL_DL) /* OpenSSL without dynamic loading */ #include #include #include #include #include #include #include #include #include #include #include #if defined(WOLFSSL_VERSION) /* Additional defines for WolfSSL, see * https://github.com/civetweb/civetweb/issues/583 */ #include "wolfssl_extras.h" #endif #if defined(OPENSSL_IS_BORINGSSL) /* From boringssl/src/include/openssl/mem.h: * * OpenSSL has, historically, had a complex set of malloc debugging options. * However, that was written in a time before Valgrind and ASAN. Since we now * have those tools, the OpenSSL allocation functions are simply macros around * the standard memory functions. * * #define OPENSSL_free free */ #define free free // disable for boringssl #define CONF_modules_unload(a) ((void)0) #define ENGINE_cleanup() ((void)0) #endif /* If OpenSSL headers are included, automatically select the API version */ #if (OPENSSL_VERSION_NUMBER >= 0x30000000L) #if !defined(OPENSSL_API_3_0) #define OPENSSL_API_3_0 #endif #define OPENSSL_REMOVE_THREAD_STATE() #else #if (OPENSSL_VERSION_NUMBER >= 0x10100000L) #if !defined(OPENSSL_API_1_1) #define OPENSSL_API_1_1 #endif #define OPENSSL_REMOVE_THREAD_STATE() #else #if !defined(OPENSSL_API_1_0) #define OPENSSL_API_1_0 #endif #define OPENSSL_REMOVE_THREAD_STATE() ERR_remove_thread_state(NULL) #endif #endif #else /* SSL loaded dynamically from DLL / shared object */ /* Add all prototypes here, to be independent from OpenSSL source * installation. */ #include "openssl_dl.h" #endif /* Various SSL bindings */ #if !defined(NO_CACHING) static const char month_names[][4] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; #endif /* !NO_CACHING */ /* Unified socket address. For IPv6 support, add IPv6 address structure in * the union u. */ union usa { struct sockaddr sa; struct sockaddr_in sin; #if defined(USE_IPV6) struct sockaddr_in6 sin6; #endif #if defined(USE_X_DOM_SOCKET) struct sockaddr_un sun; #endif }; #if defined(USE_X_DOM_SOCKET) static unsigned short USA_IN_PORT_UNSAFE(union usa *s) { if (s->sa.sa_family == AF_INET) return s->sin.sin_port; #if defined(USE_IPV6) if (s->sa.sa_family == AF_INET6) return s->sin6.sin6_port; #endif return 0; } #endif #if defined(USE_IPV6) #define USA_IN_PORT_UNSAFE(s) \ (((s)->sa.sa_family == AF_INET6) ? (s)->sin6.sin6_port : (s)->sin.sin_port) #else #define USA_IN_PORT_UNSAFE(s) ((s)->sin.sin_port) #endif /* Describes a string (chunk of memory). */ struct vec { const char *ptr; size_t len; }; struct mg_file_stat { /* File properties filled by mg_stat: */ uint64_t size; time_t last_modified; int is_directory; /* Set to 1 if mg_stat is called for a directory */ int is_gzipped; /* Set to 1 if the content is gzipped, in which * case we need a "Content-Eencoding: gzip" header */ int location; /* 0 = nowhere, 1 = on disk, 2 = in memory */ }; struct mg_file_access { /* File properties filled by mg_fopen: */ FILE *fp; }; struct mg_file { struct mg_file_stat stat; struct mg_file_access access; }; #define STRUCT_FILE_INITIALIZER \ { \ {(uint64_t)0, (time_t)0, 0, 0, 0}, \ { \ (FILE *)NULL \ } \ } /* Describes listening socket, or socket which was accept()-ed by the master * thread and queued for future handling by the worker thread. */ struct socket { SOCKET sock; /* Listening socket */ union usa lsa; /* Local socket address */ union usa rsa; /* Remote socket address */ unsigned char is_ssl; /* Is port SSL-ed */ unsigned char ssl_redir; /* Is port supposed to redirect everything to SSL * port */ unsigned char in_use; /* 0: invalid, 1: valid, 2: free */ }; /* Enum const for all options must be in sync with * static struct mg_option config_options[] * This is tested in the unit test (test/private.c) * "Private Config Options" */ enum { /* Once for each server */ LISTENING_PORTS, NUM_THREADS, RUN_AS_USER, CONFIG_TCP_NODELAY, /* Prepended CONFIG_ to avoid conflict with the * socket option typedef TCP_NODELAY. */ MAX_REQUEST_SIZE, LINGER_TIMEOUT, CONNECTION_QUEUE_SIZE, LISTEN_BACKLOG_SIZE, #if defined(__linux__) ALLOW_SENDFILE_CALL, #endif #if defined(_WIN32) CASE_SENSITIVE_FILES, #endif THROTTLE, ENABLE_KEEP_ALIVE, REQUEST_TIMEOUT, KEEP_ALIVE_TIMEOUT, #if defined(USE_WEBSOCKET) WEBSOCKET_TIMEOUT, ENABLE_WEBSOCKET_PING_PONG, #endif DECODE_URL, DECODE_QUERY_STRING, #if defined(USE_LUA) LUA_BACKGROUND_SCRIPT, LUA_BACKGROUND_SCRIPT_PARAMS, #endif #if defined(USE_HTTP2) ENABLE_HTTP2, #endif /* Once for each domain */ DOCUMENT_ROOT, ACCESS_LOG_FILE, ERROR_LOG_FILE, CGI_EXTENSIONS, CGI_ENVIRONMENT, CGI_INTERPRETER, CGI_INTERPRETER_ARGS, #if defined(USE_TIMERS) CGI_TIMEOUT, #endif CGI_BUFFERING, CGI2_EXTENSIONS, CGI2_ENVIRONMENT, CGI2_INTERPRETER, CGI2_INTERPRETER_ARGS, #if defined(USE_TIMERS) CGI2_TIMEOUT, #endif CGI2_BUFFERING, #if defined(USE_4_CGI) CGI3_EXTENSIONS, CGI3_ENVIRONMENT, CGI3_INTERPRETER, CGI3_INTERPRETER_ARGS, #if defined(USE_TIMERS) CGI3_TIMEOUT, #endif CGI3_BUFFERING, CGI4_EXTENSIONS, CGI4_ENVIRONMENT, CGI4_INTERPRETER, CGI4_INTERPRETER_ARGS, #if defined(USE_TIMERS) CGI4_TIMEOUT, #endif CGI4_BUFFERING, #endif PUT_DELETE_PASSWORDS_FILE, /* must follow CGI_* */ PROTECT_URI, AUTHENTICATION_DOMAIN, ENABLE_AUTH_DOMAIN_CHECK, SSI_EXTENSIONS, ENABLE_DIRECTORY_LISTING, ENABLE_WEBDAV, GLOBAL_PASSWORDS_FILE, INDEX_FILES, ACCESS_CONTROL_LIST, EXTRA_MIME_TYPES, SSL_CERTIFICATE, SSL_CERTIFICATE_CHAIN, URL_REWRITE_PATTERN, HIDE_FILES, SSL_DO_VERIFY_PEER, SSL_CACHE_TIMEOUT, SSL_CA_PATH, SSL_CA_FILE, SSL_VERIFY_DEPTH, SSL_DEFAULT_VERIFY_PATHS, SSL_CIPHER_LIST, SSL_PROTOCOL_VERSION, SSL_SHORT_TRUST, #if defined(USE_LUA) LUA_PRELOAD_FILE, LUA_SCRIPT_EXTENSIONS, LUA_SERVER_PAGE_EXTENSIONS, #if defined(MG_EXPERIMENTAL_INTERFACES) LUA_DEBUG_PARAMS, #endif #endif #if defined(USE_DUKTAPE) DUKTAPE_SCRIPT_EXTENSIONS, #endif #if defined(USE_WEBSOCKET) WEBSOCKET_ROOT, #endif #if defined(USE_LUA) && defined(USE_WEBSOCKET) LUA_WEBSOCKET_EXTENSIONS, #endif ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_HEADERS, ERROR_PAGES, #if !defined(NO_CACHING) STATIC_FILE_MAX_AGE, STATIC_FILE_CACHE_CONTROL, #endif #if !defined(NO_SSL) STRICT_HTTPS_MAX_AGE, #endif ADDITIONAL_HEADER, ALLOW_INDEX_SCRIPT_SUB_RES, NUM_OPTIONS }; /* Config option name, config types, default value. * Must be in the same order as the enum const above. */ static const struct mg_option config_options[] = { /* Once for each server */ {"listening_ports", MG_CONFIG_TYPE_STRING_LIST, "8080"}, {"num_threads", MG_CONFIG_TYPE_NUMBER, "50"}, {"run_as_user", MG_CONFIG_TYPE_STRING, NULL}, {"tcp_nodelay", MG_CONFIG_TYPE_NUMBER, "0"}, {"max_request_size", MG_CONFIG_TYPE_NUMBER, "16384"}, {"linger_timeout_ms", MG_CONFIG_TYPE_NUMBER, NULL}, {"connection_queue", MG_CONFIG_TYPE_NUMBER, "20"}, {"listen_backlog", MG_CONFIG_TYPE_NUMBER, "200"}, #if defined(__linux__) {"allow_sendfile_call", MG_CONFIG_TYPE_BOOLEAN, "yes"}, #endif #if defined(_WIN32) {"case_sensitive", MG_CONFIG_TYPE_BOOLEAN, "no"}, #endif {"throttle", MG_CONFIG_TYPE_STRING_LIST, NULL}, {"enable_keep_alive", MG_CONFIG_TYPE_BOOLEAN, "no"}, {"request_timeout_ms", MG_CONFIG_TYPE_NUMBER, "30000"}, {"keep_alive_timeout_ms", MG_CONFIG_TYPE_NUMBER, "500"}, #if defined(USE_WEBSOCKET) {"websocket_timeout_ms", MG_CONFIG_TYPE_NUMBER, NULL}, {"enable_websocket_ping_pong", MG_CONFIG_TYPE_BOOLEAN, "no"}, #endif {"decode_url", MG_CONFIG_TYPE_BOOLEAN, "yes"}, {"decode_query_string", MG_CONFIG_TYPE_BOOLEAN, "no"}, #if defined(USE_LUA) {"lua_background_script", MG_CONFIG_TYPE_FILE, NULL}, {"lua_background_script_params", MG_CONFIG_TYPE_STRING_LIST, NULL}, #endif #if defined(USE_HTTP2) {"enable_http2", MG_CONFIG_TYPE_BOOLEAN, "no"}, #endif /* Once for each domain */ {"document_root", MG_CONFIG_TYPE_DIRECTORY, NULL}, {"access_log_file", MG_CONFIG_TYPE_FILE, NULL}, {"error_log_file", MG_CONFIG_TYPE_FILE, NULL}, {"cgi_pattern", MG_CONFIG_TYPE_EXT_PATTERN, "**.cgi$|**.pl$|**.php$"}, {"cgi_environment", MG_CONFIG_TYPE_STRING_LIST, NULL}, {"cgi_interpreter", MG_CONFIG_TYPE_FILE, NULL}, {"cgi_interpreter_args", MG_CONFIG_TYPE_STRING, NULL}, #if defined(USE_TIMERS) {"cgi_timeout_ms", MG_CONFIG_TYPE_NUMBER, NULL}, #endif {"cgi_buffering", MG_CONFIG_TYPE_BOOLEAN, "yes"}, {"cgi2_pattern", MG_CONFIG_TYPE_EXT_PATTERN, NULL}, {"cgi2_environment", MG_CONFIG_TYPE_STRING_LIST, NULL}, {"cgi2_interpreter", MG_CONFIG_TYPE_FILE, NULL}, {"cgi2_interpreter_args", MG_CONFIG_TYPE_STRING, NULL}, #if defined(USE_TIMERS) {"cgi2_timeout_ms", MG_CONFIG_TYPE_NUMBER, NULL}, #endif {"cgi2_buffering", MG_CONFIG_TYPE_BOOLEAN, "yes"}, #if defined(USE_4_CGI) {"cgi3_pattern", MG_CONFIG_TYPE_EXT_PATTERN, NULL}, {"cgi3_environment", MG_CONFIG_TYPE_STRING_LIST, NULL}, {"cgi3_interpreter", MG_CONFIG_TYPE_FILE, NULL}, {"cgi3_interpreter_args", MG_CONFIG_TYPE_STRING, NULL}, #if defined(USE_TIMERS) {"cgi3_timeout_ms", MG_CONFIG_TYPE_NUMBER, NULL}, #endif {"cgi3_buffering", MG_CONFIG_TYPE_BOOLEAN, "yes"}, {"cgi4_pattern", MG_CONFIG_TYPE_EXT_PATTERN, NULL}, {"cgi4_environment", MG_CONFIG_TYPE_STRING_LIST, NULL}, {"cgi4_interpreter", MG_CONFIG_TYPE_FILE, NULL}, {"cgi4_interpreter_args", MG_CONFIG_TYPE_STRING, NULL}, #if defined(USE_TIMERS) {"cgi4_timeout_ms", MG_CONFIG_TYPE_NUMBER, NULL}, #endif {"cgi4_buffering", MG_CONFIG_TYPE_BOOLEAN, "yes"}, #endif {"put_delete_auth_file", MG_CONFIG_TYPE_FILE, NULL}, {"protect_uri", MG_CONFIG_TYPE_STRING_LIST, NULL}, {"authentication_domain", MG_CONFIG_TYPE_STRING, "mydomain.com"}, {"enable_auth_domain_check", MG_CONFIG_TYPE_BOOLEAN, "yes"}, {"ssi_pattern", MG_CONFIG_TYPE_EXT_PATTERN, "**.shtml$|**.shtm$"}, {"enable_directory_listing", MG_CONFIG_TYPE_BOOLEAN, "yes"}, {"enable_webdav", MG_CONFIG_TYPE_BOOLEAN, "no"}, {"global_auth_file", MG_CONFIG_TYPE_FILE, NULL}, {"index_files", MG_CONFIG_TYPE_STRING_LIST, #if defined(USE_LUA) "index.xhtml,index.html,index.htm," "index.lp,index.lsp,index.lua,index.cgi," "index.shtml,index.php"}, #else "index.xhtml,index.html,index.htm,index.cgi,index.shtml,index.php"}, #endif {"access_control_list", MG_CONFIG_TYPE_STRING_LIST, NULL}, {"extra_mime_types", MG_CONFIG_TYPE_STRING_LIST, NULL}, {"ssl_certificate", MG_CONFIG_TYPE_FILE, NULL}, {"ssl_certificate_chain", MG_CONFIG_TYPE_FILE, NULL}, {"url_rewrite_patterns", MG_CONFIG_TYPE_STRING_LIST, NULL}, {"hide_files_patterns", MG_CONFIG_TYPE_EXT_PATTERN, NULL}, {"ssl_verify_peer", MG_CONFIG_TYPE_YES_NO_OPTIONAL, "no"}, {"ssl_cache_timeout", MG_CONFIG_TYPE_NUMBER, "-1"}, {"ssl_ca_path", MG_CONFIG_TYPE_DIRECTORY, NULL}, {"ssl_ca_file", MG_CONFIG_TYPE_FILE, NULL}, {"ssl_verify_depth", MG_CONFIG_TYPE_NUMBER, "9"}, {"ssl_default_verify_paths", MG_CONFIG_TYPE_BOOLEAN, "yes"}, {"ssl_cipher_list", MG_CONFIG_TYPE_STRING, NULL}, /* HTTP2 requires ALPN, and anyway TLS1.2 should be considered * as a minimum in 2020 */ {"ssl_protocol_version", MG_CONFIG_TYPE_NUMBER, "4"}, {"ssl_short_trust", MG_CONFIG_TYPE_BOOLEAN, "no"}, #if defined(USE_LUA) {"lua_preload_file", MG_CONFIG_TYPE_FILE, NULL}, {"lua_script_pattern", MG_CONFIG_TYPE_EXT_PATTERN, "**.lua$"}, {"lua_server_page_pattern", MG_CONFIG_TYPE_EXT_PATTERN, "**.lp$|**.lsp$"}, #if defined(MG_EXPERIMENTAL_INTERFACES) {"lua_debug", MG_CONFIG_TYPE_STRING, NULL}, #endif #endif #if defined(USE_DUKTAPE) /* The support for duktape is still in alpha version state. * The name of this config option might change. */ {"duktape_script_pattern", MG_CONFIG_TYPE_EXT_PATTERN, "**.ssjs$"}, #endif #if defined(USE_WEBSOCKET) {"websocket_root", MG_CONFIG_TYPE_DIRECTORY, NULL}, #endif #if defined(USE_LUA) && defined(USE_WEBSOCKET) {"lua_websocket_pattern", MG_CONFIG_TYPE_EXT_PATTERN, "**.lua$"}, #endif {"access_control_allow_origin", MG_CONFIG_TYPE_STRING, "*"}, {"access_control_allow_methods", MG_CONFIG_TYPE_STRING, "*"}, {"access_control_allow_headers", MG_CONFIG_TYPE_STRING, "*"}, {"error_pages", MG_CONFIG_TYPE_DIRECTORY, NULL}, #if !defined(NO_CACHING) {"static_file_max_age", MG_CONFIG_TYPE_NUMBER, "3600"}, {"static_file_cache_control", MG_CONFIG_TYPE_STRING, NULL}, #endif #if !defined(NO_SSL) {"strict_transport_security_max_age", MG_CONFIG_TYPE_NUMBER, NULL}, #endif {"additional_header", MG_CONFIG_TYPE_STRING_MULTILINE, NULL}, {"allow_index_script_resource", MG_CONFIG_TYPE_BOOLEAN, "no"}, {NULL, MG_CONFIG_TYPE_UNKNOWN, NULL}}; /* Check if the config_options and the corresponding enum have compatible * sizes. */ mg_static_assert((sizeof(config_options) / sizeof(config_options[0])) == (NUM_OPTIONS + 1), "config_options and enum not sync"); enum { REQUEST_HANDLER, WEBSOCKET_HANDLER, AUTH_HANDLER }; struct mg_handler_info { /* Name/Pattern of the URI. */ char *uri; size_t uri_len; /* handler type */ int handler_type; /* Handler for http/https or requests. */ mg_request_handler handler; unsigned int refcount; int removing; /* Handler for ws/wss (websocket) requests. */ mg_websocket_connect_handler connect_handler; mg_websocket_ready_handler ready_handler; mg_websocket_data_handler data_handler; mg_websocket_close_handler close_handler; /* accepted subprotocols for ws/wss requests. */ struct mg_websocket_subprotocols *subprotocols; /* Handler for authorization requests */ mg_authorization_handler auth_handler; /* User supplied argument for the handler function. */ void *cbdata; /* next handler in a linked list */ struct mg_handler_info *next; }; enum { CONTEXT_INVALID, CONTEXT_SERVER, CONTEXT_HTTP_CLIENT, CONTEXT_WS_CLIENT }; struct mg_domain_context { SSL_CTX *ssl_ctx; /* SSL context */ char *config[NUM_OPTIONS]; /* Civetweb configuration parameters */ struct mg_handler_info *handlers; /* linked list of uri handlers */ int64_t ssl_cert_last_mtime; /* Server nonce */ uint64_t auth_nonce_mask; /* Mask for all nonce values */ unsigned long nonce_count; /* Used nonces, used for authentication */ #if defined(USE_LUA) && defined(USE_WEBSOCKET) /* linked list of shared lua websockets */ struct mg_shared_lua_websocket_list *shared_lua_websockets; #endif /* Linked list of domains */ struct mg_domain_context *next; }; /* Stop flag can be "volatile" or require a lock. * MSDN uses volatile for "Interlocked" operations, but also explicitly * states a read operation for int is always atomic. */ #if defined(STOP_FLAG_NEEDS_LOCK) typedef ptrdiff_t volatile stop_flag_t; static int STOP_FLAG_IS_ZERO(const stop_flag_t *f) { stop_flag_t sf = mg_atomic_add((stop_flag_t *)f, 0); return (sf == 0); } static int STOP_FLAG_IS_TWO(stop_flag_t *f) { stop_flag_t sf = mg_atomic_add(f, 0); return (sf == 2); } static void STOP_FLAG_ASSIGN(stop_flag_t *f, stop_flag_t v) { stop_flag_t sf; do { sf = mg_atomic_compare_and_swap(f, *f, v); } while (sf != v); } #else /* STOP_FLAG_NEEDS_LOCK */ typedef int volatile stop_flag_t; #define STOP_FLAG_IS_ZERO(f) ((*(f)) == 0) #define STOP_FLAG_IS_TWO(f) ((*(f)) == 2) #define STOP_FLAG_ASSIGN(f, v) ((*(f)) = (v)) #endif /* STOP_FLAG_NEEDS_LOCK */ #if !defined(NUM_WEBDAV_LOCKS) #define NUM_WEBDAV_LOCKS 10 #endif #if !defined(LOCK_DURATION_S) #define LOCK_DURATION_S 60 #endif struct twebdav_lock { uint64_t locktime; char token[33]; char path[UTF8_PATH_MAX * 2]; char user[UTF8_PATH_MAX * 2]; }; struct mg_context { /* Part 1 - Physical context: * This holds threads, ports, timeouts, ... * set for the entire server, independent from the * addressed hostname. */ /* Connection related */ int context_type; /* See CONTEXT_* above */ struct socket *listening_sockets; struct mg_pollfd *listening_socket_fds; unsigned int num_listening_sockets; struct mg_connection *worker_connections; /* The connection struct, pre- * allocated for each worker */ #if defined(USE_SERVER_STATS) volatile ptrdiff_t active_connections; volatile ptrdiff_t max_active_connections; volatile ptrdiff_t total_connections; volatile ptrdiff_t total_requests; volatile int64_t total_data_read; volatile int64_t total_data_written; #endif /* Thread related */ stop_flag_t stop_flag; /* Should we stop event loop */ pthread_mutex_t thread_mutex; /* Protects client_socks or queue */ pthread_t masterthreadid; /* The master thread ID */ unsigned int cfg_worker_threads; /* The number of configured worker threads. */ pthread_t *worker_threadids; /* The worker thread IDs */ unsigned long starter_thread_idx; /* thread index which called mg_start */ /* Connection to thread dispatching */ #if defined(ALTERNATIVE_QUEUE) struct socket *client_socks; void **client_wait_events; #else struct socket *squeue; /* Socket queue (sq) : accepted sockets waiting for a worker thread */ volatile int sq_head; /* Head of the socket queue */ volatile int sq_tail; /* Tail of the socket queue */ pthread_cond_t sq_full; /* Signaled when socket is produced */ pthread_cond_t sq_empty; /* Signaled when socket is consumed */ volatile int sq_blocked; /* Status information: sq is full */ int sq_size; /* No of elements in socket queue */ #if defined(USE_SERVER_STATS) int sq_max_fill; #endif /* USE_SERVER_STATS */ #endif /* ALTERNATIVE_QUEUE */ /* Memory related */ unsigned int max_request_size; /* The max request size */ #if defined(USE_SERVER_STATS) struct mg_memory_stat ctx_memory; #endif /* WebDAV lock structures */ struct twebdav_lock webdav_lock[NUM_WEBDAV_LOCKS]; /* Operating system related */ char *systemName; /* What operating system is running */ time_t start_time; /* Server start time, used for authentication * and for diagnstics. */ #if defined(USE_TIMERS) struct ttimers *timers; #endif /* Lua specific: Background operations and shared websockets */ #if defined(USE_LUA) void *lua_background_state; /* lua_State (here as void *) */ pthread_mutex_t lua_bg_mutex; /* Protect background state */ int lua_bg_log_available; /* Use Lua background state for access log */ #endif /* Server nonce */ pthread_mutex_t nonce_mutex; /* Protects ssl_ctx, handlers, * ssl_cert_last_mtime, nonce_count, and * next (linked list) */ /* Server callbacks */ struct mg_callbacks callbacks; /* User-defined callback function */ void *user_data; /* User-defined data */ /* Part 2 - Logical domain: * This holds hostname, TLS certificate, document root, ... * set for a domain hosted at the server. * There may be multiple domains hosted at one physical server. * The default domain "dd" is the first element of a list of * domains. */ struct mg_domain_context dd; /* default domain */ }; #if defined(USE_SERVER_STATS) static struct mg_memory_stat mg_common_memory = {0, 0, 0}; static struct mg_memory_stat * get_memory_stat(struct mg_context *ctx) { if (ctx) { return &(ctx->ctx_memory); } return &mg_common_memory; } #endif enum { CONNECTION_TYPE_INVALID = 0, CONNECTION_TYPE_REQUEST = 1, CONNECTION_TYPE_RESPONSE = 2 }; enum { PROTOCOL_TYPE_HTTP1 = 0, PROTOCOL_TYPE_WEBSOCKET = 1, PROTOCOL_TYPE_HTTP2 = 2 }; #if defined(USE_HTTP2) #if !defined(HTTP2_DYN_TABLE_SIZE) #define HTTP2_DYN_TABLE_SIZE (256) #endif struct mg_http2_connection { uint32_t stream_id; uint32_t dyn_table_size; struct mg_header dyn_table[HTTP2_DYN_TABLE_SIZE]; }; #endif struct mg_connection { int connection_type; /* see CONNECTION_TYPE_* above */ int protocol_type; /* see PROTOCOL_TYPE_*: 0=http/1.x, 1=ws, 2=http/2 */ int request_state; /* 0: nothing sent, 1: header partially sent, 2: header fully sent */ #if defined(USE_HTTP2) struct mg_http2_connection http2; #endif struct mg_request_info request_info; struct mg_response_info response_info; struct mg_context *phys_ctx; struct mg_domain_context *dom_ctx; #if defined(USE_SERVER_STATS) int conn_state; /* 0 = undef, numerical value may change in different * versions. For the current definition, see * mg_get_connection_info_impl */ #endif SSL *ssl; /* SSL descriptor */ struct socket client; /* Connected client */ time_t conn_birth_time; /* Time (wall clock) when connection was * established */ #if defined(USE_SERVER_STATS) time_t conn_close_time; /* Time (wall clock) when connection was * closed (or 0 if still open) */ double processing_time; /* Processing time for one request. */ #endif struct timespec req_time; /* Time (since system start) when the request * was received */ int64_t num_bytes_sent; /* Total bytes sent to client */ int64_t content_len; /* How many bytes of content can be read * !is_chunked: Content-Length header value * or -1 (until connection closed, * not allowed for a request) * is_chunked: >= 0, appended gradually */ int64_t consumed_content; /* How many bytes of content have been read */ int is_chunked; /* Transfer-Encoding is chunked: * 0 = not chunked, * 1 = chunked, not yet, or some data read, * 2 = chunked, has error, * 3 = chunked, all data read except trailer, * 4 = chunked, all data read */ char *buf; /* Buffer for received data */ char *path_info; /* PATH_INFO part of the URL */ int must_close; /* 1 if connection must be closed */ int accept_gzip; /* 1 if gzip encoding is accepted */ int in_error_handler; /* 1 if in handler for user defined error * pages */ #if defined(USE_WEBSOCKET) int in_websocket_handling; /* 1 if in read_websocket */ #endif #if defined(USE_ZLIB) && defined(USE_WEBSOCKET) \ && defined(MG_EXPERIMENTAL_INTERFACES) /* Parameters for websocket data compression according to rfc7692 */ int websocket_deflate_server_max_windows_bits; int websocket_deflate_client_max_windows_bits; int websocket_deflate_server_no_context_takeover; int websocket_deflate_client_no_context_takeover; int websocket_deflate_initialized; int websocket_deflate_flush; z_stream websocket_deflate_state; z_stream websocket_inflate_state; #endif int handled_requests; /* Number of requests handled by this connection */ int buf_size; /* Buffer size */ int request_len; /* Size of the request + headers in a buffer */ int data_len; /* Total size of data in a buffer */ int status_code; /* HTTP reply status code, e.g. 200 */ int throttle; /* Throttling, bytes/sec. <= 0 means no * throttle */ time_t last_throttle_time; /* Last time throttled data was sent */ int last_throttle_bytes; /* Bytes sent this second */ pthread_mutex_t mutex; /* Used by mg_(un)lock_connection to ensure * atomic transmissions for websockets */ #if defined(USE_LUA) && defined(USE_WEBSOCKET) void *lua_websocket_state; /* Lua_State for a websocket connection */ #endif void *tls_user_ptr; /* User defined pointer in thread local storage, * for quick access */ }; /* Directory entry */ struct de { char *file_name; struct mg_file_stat file; }; #define mg_cry_internal(conn, fmt, ...) \ mg_cry_internal_wrap(conn, NULL, __func__, __LINE__, fmt, __VA_ARGS__) #define mg_cry_ctx_internal(ctx, fmt, ...) \ mg_cry_internal_wrap(NULL, ctx, __func__, __LINE__, fmt, __VA_ARGS__) static void mg_cry_internal_wrap(const struct mg_connection *conn, struct mg_context *ctx, const char *func, unsigned line, const char *fmt, ...) PRINTF_ARGS(5, 6); #if !defined(NO_THREAD_NAME) #if defined(_WIN32) && defined(_MSC_VER) /* Set the thread name for debugging purposes in Visual Studio * http://msdn.microsoft.com/en-us/library/xcb2z8hs.aspx */ # pragma pack(push, 8) typedef struct tagTHREADNAME_INFO { DWORD dwType; /* Must be 0x1000. */ LPCSTR szName; /* Pointer to name (in user addr space). */ DWORD dwThreadID; /* Thread ID (-1=caller thread). */ DWORD dwFlags; /* Reserved for future use, must be zero. */ } THREADNAME_INFO; # pragma pack(pop) #elif defined(__linux__) #include #include #if defined(ALTERNATIVE_QUEUE) #include #endif /* ALTERNATIVE_QUEUE */ #if defined(ALTERNATIVE_QUEUE) static void * event_create(void) { int evhdl = eventfd(0, EFD_CLOEXEC); int *ret; if (evhdl == -1) { /* Linux uses -1 on error, Windows NULL. */ /* However, Linux does not return 0 on success either. */ return 0; } ret = (int *)mg_malloc(sizeof(int)); if (ret) { *ret = evhdl; } else { (void)close(evhdl); } return (void *)ret; } static int event_wait(void *eventhdl) { uint64_t u; int evhdl, s; if (!eventhdl) { /* error */ return 0; } evhdl = *(int *)eventhdl; s = (int)read(evhdl, &u, sizeof(u)); if (s != sizeof(u)) { /* error */ return 0; } (void)u; /* the value is not required */ return 1; } static int event_signal(void *eventhdl) { uint64_t u = 1; int evhdl, s; if (!eventhdl) { /* error */ return 0; } evhdl = *(int *)eventhdl; s = (int)write(evhdl, &u, sizeof(u)); if (s != sizeof(u)) { /* error */ return 0; } return 1; } static void event_destroy(void *eventhdl) { int evhdl; if (!eventhdl) { /* error */ return; } evhdl = *(int *)eventhdl; close(evhdl); mg_free(eventhdl); } #endif #endif #if !defined(__linux__) && !defined(_WIN32) && defined(ALTERNATIVE_QUEUE) struct posix_event { pthread_mutex_t mutex; pthread_cond_t cond; int signaled; }; static void * event_create(void) { struct posix_event *ret = mg_malloc(sizeof(struct posix_event)); if (ret == 0) { /* out of memory */ return 0; } if (0 != pthread_mutex_init(&(ret->mutex), NULL)) { /* pthread mutex not available */ mg_free(ret); return 0; } if (0 != pthread_cond_init(&(ret->cond), NULL)) { /* pthread cond not available */ pthread_mutex_destroy(&(ret->mutex)); mg_free(ret); return 0; } ret->signaled = 0; return (void *)ret; } static int event_wait(void *eventhdl) { struct posix_event *ev = (struct posix_event *)eventhdl; pthread_mutex_lock(&(ev->mutex)); while (!ev->signaled) { pthread_cond_wait(&(ev->cond), &(ev->mutex)); } ev->signaled = 0; pthread_mutex_unlock(&(ev->mutex)); return 1; } static int event_signal(void *eventhdl) { struct posix_event *ev = (struct posix_event *)eventhdl; pthread_mutex_lock(&(ev->mutex)); pthread_cond_signal(&(ev->cond)); ev->signaled = 1; pthread_mutex_unlock(&(ev->mutex)); return 1; } static void event_destroy(void *eventhdl) { struct posix_event *ev = (struct posix_event *)eventhdl; pthread_cond_destroy(&(ev->cond)); pthread_mutex_destroy(&(ev->mutex)); mg_free(ev); } #endif static void mg_set_thread_name(const char *name) { char threadName[16 + 1]; /* 16 = Max. thread length in Linux/OSX/.. */ mg_snprintf( NULL, NULL, threadName, sizeof(threadName), "civetweb-%s", name); #if defined(_WIN32) #if defined(_MSC_VER) /* Windows and Visual Studio Compiler */ __try { THREADNAME_INFO info; info.dwType = 0x1000; info.szName = threadName; info.dwThreadID = ~0U; info.dwFlags = 0; RaiseException(0x406D1388, 0, sizeof(info) / sizeof(ULONG_PTR), (ULONG_PTR *)&info); } __except (EXCEPTION_EXECUTE_HANDLER) { } #elif defined(__MINGW32__) /* No option known to set thread name for MinGW known */ #endif #elif defined(_GNU_SOURCE) && defined(__GLIBC__) \ && ((__GLIBC__ > 2) || ((__GLIBC__ == 2) && (__GLIBC_MINOR__ >= 12))) /* pthread_setname_np first appeared in glibc in version 2.12 */ #if defined(__MACH__) && defined(__APPLE__) /* OS X only current thread name can be changed */ (void)pthread_setname_np(threadName); #else (void)pthread_setname_np(pthread_self(), threadName); #endif #elif defined(__linux__) /* On Linux we can use the prctl function. * When building for Linux Standard Base (LSB) use * NO_THREAD_NAME. However, thread names are a big * help for debugging, so the stadard is to set them. */ (void)prctl(PR_SET_NAME, threadName, 0, 0, 0); #endif } #else /* !defined(NO_THREAD_NAME) */ static void mg_set_thread_name(const char *threadName) { } #endif CIVETWEB_API const struct mg_option * mg_get_valid_options(void) { return config_options; } /* Do not open file (unused) */ #define MG_FOPEN_MODE_NONE (0) /* Open file for read only access */ #define MG_FOPEN_MODE_READ (1) /* Open file for writing, create and overwrite */ #define MG_FOPEN_MODE_WRITE (2) /* Open file for writing, create and append */ #define MG_FOPEN_MODE_APPEND (4) static int is_file_opened(const struct mg_file_access *fileacc) { if (!fileacc) { return 0; } return (fileacc->fp != NULL); } #if !defined(NO_FILESYSTEMS) static int mg_stat(const struct mg_connection *conn, const char *path, struct mg_file_stat *filep); /* Reject files with special characters (for Windows) */ static int mg_path_suspicious(const struct mg_connection *conn, const char *path) { const uint8_t *c = (const uint8_t *)path; (void)conn; /* not used */ if ((c == NULL) || (c[0] == 0)) { /* Null pointer or empty path --> suspicious */ return 1; } #if defined(_WIN32) while (*c) { if (*c < 32) { /* Control character */ return 1; } if ((*c == '>') || (*c == '<') || (*c == '|')) { /* stdin/stdout redirection character */ return 1; } if ((*c == '*') || (*c == '?')) { /* Wildcard character */ return 1; } if (*c == '"') { /* Windows quotation */ return 1; } c++; } #endif /* Nothing suspicious found */ return 0; } /* mg_fopen will open a file either in memory or on the disk. * The input parameter path is a string in UTF-8 encoding. * The input parameter mode is MG_FOPEN_MODE_* * On success, fp will be set in the output struct mg_file. * All status members will also be set. * The function returns 1 on success, 0 on error. */ static int mg_fopen(const struct mg_connection *conn, const char *path, int mode, struct mg_file *filep) { int found; if (!filep) { return 0; } filep->access.fp = NULL; if (mg_path_suspicious(conn, path)) { return 0; } /* filep is initialized in mg_stat: all fields with memset to, * some fields like size and modification date with values */ found = mg_stat(conn, path, &(filep->stat)); if ((mode == MG_FOPEN_MODE_READ) && (!found)) { /* file does not exist and will not be created */ return 0; } #if defined(_WIN32) { wchar_t wbuf[UTF16_PATH_MAX]; path_to_unicode(conn, path, wbuf, ARRAY_SIZE(wbuf)); switch (mode) { case MG_FOPEN_MODE_READ: filep->access.fp = _wfopen(wbuf, L"rb"); break; case MG_FOPEN_MODE_WRITE: filep->access.fp = _wfopen(wbuf, L"wb"); break; case MG_FOPEN_MODE_APPEND: filep->access.fp = _wfopen(wbuf, L"ab"); break; } } #else /* Linux et al already use unicode. No need to convert. */ switch (mode) { case MG_FOPEN_MODE_READ: filep->access.fp = fopen(path, "r"); break; case MG_FOPEN_MODE_WRITE: filep->access.fp = fopen(path, "w"); break; case MG_FOPEN_MODE_APPEND: filep->access.fp = fopen(path, "a"); break; } #endif if (!found) { /* File did not exist before fopen was called. * Maybe it has been created now. Get stat info * like creation time now. */ found = mg_stat(conn, path, &(filep->stat)); (void)found; } /* return OK if file is opened */ return (filep->access.fp != NULL); } /* return 0 on success, just like fclose */ static int mg_fclose(struct mg_file_access *fileacc) { int ret = -1; if (fileacc != NULL) { if (fileacc->fp != NULL) { ret = fclose(fileacc->fp); } /* reset all members of fileacc */ memset(fileacc, 0, sizeof(*fileacc)); } return ret; } #endif /* NO_FILESYSTEMS */ static void mg_strlcpy(char *dst, const char *src, size_t n) { for (; *src != '\0' && n > 1; n--) { *dst++ = *src++; } *dst = '\0'; } static int lowercase(const char *s) { return tolower((unsigned char)*s); } CIVETWEB_API int mg_strncasecmp(const char *s1, const char *s2, size_t len) { int diff = 0; if (len > 0) { do { diff = lowercase(s1++) - lowercase(s2++); } while (diff == 0 && s1[-1] != '\0' && --len > 0); } return diff; } CIVETWEB_API int mg_strcasecmp(const char *s1, const char *s2) { int diff; do { diff = lowercase(s1++) - lowercase(s2++); } while (diff == 0 && s1[-1] != '\0'); return diff; } static char * mg_strndup_ctx(const char *ptr, size_t len, struct mg_context *ctx) { char *p; (void)ctx; /* Avoid Visual Studio warning if USE_SERVER_STATS is not * defined */ if ((p = (char *)mg_malloc_ctx(len + 1, ctx)) != NULL) { mg_strlcpy(p, ptr, len + 1); } return p; } static char * mg_strdup_ctx(const char *str, struct mg_context *ctx) { return mg_strndup_ctx(str, strlen(str), ctx); } static char * mg_strdup(const char *str) { return mg_strndup_ctx(str, strlen(str), NULL); } static const char * mg_strcasestr(const char *big_str, const char *small_str) { size_t i, big_len = strlen(big_str), small_len = strlen(small_str); if (big_len >= small_len) { for (i = 0; i <= (big_len - small_len); i++) { if (mg_strncasecmp(big_str + i, small_str, small_len) == 0) { return big_str + i; } } } return NULL; } /* Return null terminated string of given maximum length. * Report errors if length is exceeded. */ static void mg_vsnprintf(const struct mg_connection *conn, int *truncated, char *buf, size_t buflen, const char *fmt, va_list ap) { int n, ok; if (buflen == 0) { if (truncated) { *truncated = 1; } return; } #if defined(__clang__) # pragma clang diagnostic push # pragma clang diagnostic ignored "-Wformat-nonliteral" /* Using fmt as a non-literal is intended here, since it is mostly called * indirectly by mg_snprintf */ #endif n = (int)vsnprintf_impl(buf, buflen, fmt, ap); ok = (n >= 0) && ((size_t)n < buflen); #if defined(__clang__) # pragma clang diagnostic pop #endif if (ok) { if (truncated) { *truncated = 0; } } else { if (truncated) { *truncated = 1; } mg_cry_internal(conn, "truncating vsnprintf buffer: [%.*s]", (int)((buflen > 200) ? 200 : (buflen - 1)), buf); n = (int)buflen - 1; } buf[n] = '\0'; } static void mg_snprintf(const struct mg_connection *conn, int *truncated, char *buf, size_t buflen, const char *fmt, ...) { va_list ap; va_start(ap, fmt); mg_vsnprintf(conn, truncated, buf, buflen, fmt, ap); va_end(ap); } static int get_option_index(const char *name) { int i; for (i = 0; config_options[i].name != NULL; i++) { if (strcmp(config_options[i].name, name) == 0) { return i; } } return -1; } CIVETWEB_API const char * mg_get_option(const struct mg_context *ctx, const char *name) { int i; if ((i = get_option_index(name)) == -1) { return NULL; } else if (!ctx || ctx->dd.config[i] == NULL) { return ""; } else { return ctx->dd.config[i]; } } #define mg_get_option DO_NOT_USE_THIS_FUNCTION_INTERNALLY__access_directly CIVETWEB_API struct mg_context * mg_get_context(const struct mg_connection *conn) { return (conn == NULL) ? (struct mg_context *)NULL : (conn->phys_ctx); } CIVETWEB_API void * mg_get_user_data(const struct mg_context *ctx) { return (ctx == NULL) ? NULL : ctx->user_data; } CIVETWEB_API void * mg_get_user_context_data(const struct mg_connection *conn) { return mg_get_user_data(mg_get_context(conn)); } CIVETWEB_API void * mg_get_thread_pointer(const struct mg_connection *conn) { /* both methods should return the same pointer */ if (conn) { /* quick access, in case conn is known */ return conn->tls_user_ptr; } else { /* otherwise get pointer from thread local storage (TLS) */ struct mg_workerTLS *tls = (struct mg_workerTLS *)pthread_getspecific(sTlsKey); return tls->user_ptr; } } CIVETWEB_API void mg_set_user_connection_data(const struct mg_connection *const_conn, void *data) { if (const_conn != NULL) { /* Const cast, since "const struct mg_connection *" does not mean * the connection object is not modified. Here "const" is used, * to indicate mg_read/mg_write/mg_send/.. must not be called. */ struct mg_connection *conn = (struct mg_connection *)const_conn; conn->request_info.conn_data = data; } } CIVETWEB_API void * mg_get_user_connection_data(const struct mg_connection *conn) { if (conn != NULL) { return conn->request_info.conn_data; } return NULL; } CIVETWEB_API int mg_get_server_ports(const struct mg_context *ctx, int size, struct mg_server_port *ports) { int i, cnt = 0; if (size <= 0) { return -1; } memset(ports, 0, sizeof(*ports) * (size_t)size); if (!ctx) { return -1; } if (!ctx->listening_sockets) { return -1; } for (i = 0; (i < size) && (i < (int)ctx->num_listening_sockets); i++) { ports[cnt].port = ntohs(USA_IN_PORT_UNSAFE(&(ctx->listening_sockets[i].lsa))); ports[cnt].is_ssl = ctx->listening_sockets[i].is_ssl; ports[cnt].is_redirect = ctx->listening_sockets[i].ssl_redir; if (ctx->listening_sockets[i].lsa.sa.sa_family == AF_INET) { /* IPv4 */ ports[cnt].protocol = 1; cnt++; } else if (ctx->listening_sockets[i].lsa.sa.sa_family == AF_INET6) { /* IPv6 */ ports[cnt].protocol = 3; cnt++; } } return cnt; } #if defined(USE_X_DOM_SOCKET) && !defined(UNIX_DOMAIN_SOCKET_SERVER_NAME) #define UNIX_DOMAIN_SOCKET_SERVER_NAME "*" #endif static void sockaddr_to_string(char *buf, size_t len, const union usa *usa) { buf[0] = '\0'; if (!usa) { return; } if (usa->sa.sa_family == AF_INET) { getnameinfo(&usa->sa, sizeof(usa->sin), buf, (unsigned)len, NULL, 0, NI_NUMERICHOST); } #if defined(USE_IPV6) else if (usa->sa.sa_family == AF_INET6) { getnameinfo(&usa->sa, sizeof(usa->sin6), buf, (unsigned)len, NULL, 0, NI_NUMERICHOST); } #endif #if defined(USE_X_DOM_SOCKET) else if (usa->sa.sa_family == AF_UNIX) { /* TODO: Define a remote address for unix domain sockets. * This code will always return "localhost", identical to http+tcp: getnameinfo(&usa->sa, sizeof(usa->sun), buf, (unsigned)len, NULL, 0, NI_NUMERICHOST); */ mg_strlcpy(buf, UNIX_DOMAIN_SOCKET_SERVER_NAME, len); } #endif } /* Convert time_t to a string. According to RFC2616, Sec 14.18, this must be * included in all responses other than 100, 101, 5xx. */ static void gmt_time_string(char *buf, size_t buf_len, time_t *t) { #if !defined(REENTRANT_TIME) struct tm *tm; tm = ((t != NULL) ? gmtime(t) : NULL); if (tm != NULL) { #else struct tm _tm; struct tm *tm = &_tm; if (t != NULL) { gmtime_r(t, tm); #endif strftime(buf, buf_len, "%a, %d %b %Y %H:%M:%S GMT", tm); } else { mg_strlcpy(buf, "Thu, 01 Jan 1970 00:00:00 GMT", buf_len); } } /* difftime for struct timespec. Return value is in seconds. */ static double mg_difftimespec(const struct timespec *ts_now, const struct timespec *ts_before) { return (double)(ts_now->tv_nsec - ts_before->tv_nsec) * 1.0E-9 + (double)(ts_now->tv_sec - ts_before->tv_sec); } #if defined(MG_EXTERNAL_FUNCTION_mg_cry_internal_impl) static void mg_cry_internal_impl(const struct mg_connection *conn, const char *func, unsigned line, const char *fmt, va_list ap); #include "external_mg_cry_internal_impl.h" #elif !defined(NO_FILESYSTEMS) /* Print error message to the opened error log stream. */ static void mg_cry_internal_impl(const struct mg_connection *conn, const char *func, unsigned line, const char *fmt, va_list ap) { char buf[MG_BUF_LEN], src_addr[IP_ADDR_STR_LEN]; struct mg_file fi; time_t timestamp; /* Unused, in the RELEASE build */ (void)func; (void)line; #if defined(GCC_DIAGNOSTIC) # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wformat-nonliteral" #endif IGNORE_UNUSED_RESULT(vsnprintf_impl(buf, sizeof(buf), fmt, ap)); #if defined(GCC_DIAGNOSTIC) # pragma GCC diagnostic pop #endif buf[sizeof(buf) - 1] = 0; DEBUG_TRACE("mg_cry called from %s:%u: %s", func, line, buf); if (!conn) { /* puts(buf) */ return; } /* Do not lock when getting the callback value, here and below. * I suppose this is fine, since function cannot disappear in the * same way string option can. */ if ((conn->phys_ctx->callbacks.log_message == NULL) || (conn->phys_ctx->callbacks.log_message(conn, buf) == 0)) { if (conn->dom_ctx->config[ERROR_LOG_FILE] != NULL) { if (mg_fopen(conn, conn->dom_ctx->config[ERROR_LOG_FILE], MG_FOPEN_MODE_APPEND, &fi) == 0) { fi.access.fp = NULL; } } else { fi.access.fp = NULL; } if (fi.access.fp != NULL) { flockfile(fi.access.fp); timestamp = time(NULL); sockaddr_to_string(src_addr, sizeof(src_addr), &conn->client.rsa); fprintf(fi.access.fp, "[%010lu] [error] [client %s] ", (unsigned long)timestamp, src_addr); if (conn->request_info.request_method != NULL) { fprintf(fi.access.fp, "%s %s: ", conn->request_info.request_method, conn->request_info.request_uri ? conn->request_info.request_uri : ""); } fprintf(fi.access.fp, "%s", buf); fputc('\n', fi.access.fp); fflush(fi.access.fp); funlockfile(fi.access.fp); (void)mg_fclose(&fi.access); /* Ignore errors. We can't call * mg_cry here anyway ;-) */ } } } #else #error Must either enable filesystems or provide a custom mg_cry_internal_impl implementation #endif /* Externally provided function */ /* Construct fake connection structure. Used for logging, if connection * is not applicable at the moment of logging. */ static struct mg_connection * fake_connection(struct mg_connection *fc, struct mg_context *ctx) { static const struct mg_connection conn_zero = {0}; *fc = conn_zero; fc->phys_ctx = ctx; fc->dom_ctx = &(ctx->dd); return fc; } static void mg_cry_internal_wrap(const struct mg_connection *conn, struct mg_context *ctx, const char *func, unsigned line, const char *fmt, ...) { va_list ap; va_start(ap, fmt); if (!conn && ctx) { struct mg_connection fc; mg_cry_internal_impl(fake_connection(&fc, ctx), func, line, fmt, ap); } else { mg_cry_internal_impl(conn, func, line, fmt, ap); } va_end(ap); } CIVETWEB_API void mg_cry(const struct mg_connection *conn, const char *fmt, ...) { va_list ap; va_start(ap, fmt); mg_cry_internal_impl(conn, "user", 0, fmt, ap); va_end(ap); } #define mg_cry DO_NOT_USE_THIS_FUNCTION__USE_mg_cry_internal CIVETWEB_API const char * mg_version(void) { return CIVETWEB_VERSION; } CIVETWEB_API const struct mg_request_info * mg_get_request_info(const struct mg_connection *conn) { if (!conn) { return NULL; } #if defined(MG_ALLOW_USING_GET_REQUEST_INFO_FOR_RESPONSE) if (conn->connection_type == CONNECTION_TYPE_RESPONSE) { char txt[16]; struct mg_workerTLS *tls = (struct mg_workerTLS *)pthread_getspecific(sTlsKey); snprintf(txt, "%03i", sizeof(txt), conn->response_info.status_code); if (strlen(txt) == 3) { memcpy(tls->txtbuf, txt, 4); } else { strcpy(tls->txtbuf, "ERR"); } ((struct mg_connection *)conn)->request_info.local_uri = tls->txtbuf; /* use thread safe buffer */ ((struct mg_connection *)conn)->request_info.local_uri_raw = tls->txtbuf; /* use the same thread safe buffer */ ((struct mg_connection *)conn)->request_info.request_uri = tls->txtbuf; /* use the same thread safe buffer */ ((struct mg_connection *)conn)->request_info.num_headers = conn->response_info.num_headers; memcpy(((struct mg_connection *)conn)->request_info.http_headers, conn->response_info.http_headers, sizeof(conn->response_info.http_headers)); } else #endif if (conn->connection_type != CONNECTION_TYPE_REQUEST) { return NULL; } return &conn->request_info; } CIVETWEB_API const struct mg_response_info * mg_get_response_info(const struct mg_connection *conn) { if (!conn) { return NULL; } if (conn->connection_type != CONNECTION_TYPE_RESPONSE) { return NULL; } return &conn->response_info; } static const char * get_proto_name(const struct mg_connection *conn) { #if defined(__clang__) # pragma clang diagnostic push # pragma clang diagnostic ignored "-Wunreachable-code" /* Depending on USE_WEBSOCKET and NO_SSL, some oft the protocols might be * not supported. Clang raises an "unreachable code" warning for parts of ?: * unreachable, but splitting into four different #ifdef clauses here is * more complicated. */ #endif const struct mg_request_info *ri = &conn->request_info; const char *proto = ((conn->protocol_type == PROTOCOL_TYPE_WEBSOCKET) ? (ri->is_ssl ? "wss" : "ws") : (ri->is_ssl ? "https" : "http")); return proto; #if defined(__clang__) # pragma clang diagnostic pop #endif } static int mg_construct_local_link(const struct mg_connection *conn, char *buf, size_t buflen, const char *define_proto, int define_port, const char *define_uri) { if ((buflen < 1) || (buf == 0) || (conn == 0)) { return -1; } else { int i, j; int truncated = 0; const struct mg_request_info *ri = &conn->request_info; const char *proto = (define_proto != NULL) ? define_proto : get_proto_name(conn); const char *uri = (define_uri != NULL) ? define_uri : ((ri->request_uri != NULL) ? ri->request_uri : ri->local_uri); int port = (define_port > 0) ? define_port : ri->server_port; int default_port = 80; char *uri_encoded; size_t uri_encoded_len; if (uri == NULL) { return -1; } uri_encoded_len = strlen(uri) * 3 + 1; uri_encoded = (char *)mg_malloc_ctx(uri_encoded_len, conn->phys_ctx); if (uri_encoded == NULL) { return -1; } mg_url_encode(uri, uri_encoded, uri_encoded_len); /* Directory separator should be preserved. */ for (i = j = 0; uri_encoded[i]; j++) { if (!strncmp(uri_encoded + i, "%2f", 3)) { uri_encoded[j] = '/'; i += 3; } else { uri_encoded[j] = uri_encoded[i++]; } } uri_encoded[j] = '\0'; #if defined(USE_X_DOM_SOCKET) if (conn->client.lsa.sa.sa_family == AF_UNIX) { /* TODO: Define and document a link for UNIX domain sockets. */ /* There seems to be no official standard for this. * Common uses seem to be "httpunix://", "http.unix://" or * "http+unix://" as a protocol definition string, followed by * "localhost" or "127.0.0.1" or "/tmp/unix/path" or * "%2Ftmp%2Funix%2Fpath" (url % encoded) or * "localhost:%2Ftmp%2Funix%2Fpath" (domain socket path as port) or * "" (completely skipping the server name part). In any case, the * last part is the server local path. */ const char *server_name = UNIX_DOMAIN_SOCKET_SERVER_NAME; mg_snprintf(conn, &truncated, buf, buflen, "%s.unix://%s%s", proto, server_name, ri->local_uri); default_port = 0; mg_free(uri_encoded); return 0; } #endif if (define_proto) { /* If we got a protocol name, use the default port accordingly. */ if ((0 == strcmp(define_proto, "https")) || (0 == strcmp(define_proto, "wss"))) { default_port = 443; } } else if (ri->is_ssl) { /* If we did not get a protocol name, use TLS as default if it is * already used. */ default_port = 443; } { #if defined(USE_IPV6) int is_ipv6 = (conn->client.lsa.sa.sa_family == AF_INET6); #endif int auth_domain_check_enabled = conn->dom_ctx->config[ENABLE_AUTH_DOMAIN_CHECK] && (!mg_strcasecmp( conn->dom_ctx->config[ENABLE_AUTH_DOMAIN_CHECK], "yes")); const char *server_domain = conn->dom_ctx->config[AUTHENTICATION_DOMAIN]; char portstr[16]; char server_ip[48]; if (port != default_port) { snprintf(portstr, sizeof(portstr), ":%u", (unsigned)port); } else { portstr[0] = 0; } if (!auth_domain_check_enabled || !server_domain) { sockaddr_to_string(server_ip, sizeof(server_ip), &conn->client.lsa); server_domain = server_ip; } mg_snprintf(conn, &truncated, buf, buflen, #if defined(USE_IPV6) "%s://%s%s%s%s%s", proto, (is_ipv6 && (server_domain == server_ip)) ? "[" : "", server_domain, (is_ipv6 && (server_domain == server_ip)) ? "]" : "", #else "%s://%s%s%s", proto, server_domain, #endif portstr, uri_encoded); mg_free(uri_encoded); if (truncated) { return -1; } return 0; } } } CIVETWEB_API int mg_get_request_link(const struct mg_connection *conn, char *buf, size_t buflen) { return mg_construct_local_link(conn, buf, buflen, NULL, -1, NULL); } /* Skip the characters until one of the delimiters characters found. * 0-terminate resulting word. Skip the delimiter and following whitespaces. * Advance pointer to buffer to the next word. Return found 0-terminated * word. * Delimiters can be quoted with quotechar. */ static char * skip_quoted(char **buf, const char *delimiters, const char *whitespace, char quotechar) { char *p, *begin_word, *end_word, *end_whitespace; begin_word = *buf; end_word = begin_word + strcspn(begin_word, delimiters); /* Check for quotechar */ if (end_word > begin_word) { p = end_word - 1; while (*p == quotechar) { /* While the delimiter is quoted, look for the next delimiter. */ /* This happens, e.g., in calls from parse_auth_header, * if the user name contains a " character. */ /* If there is anything beyond end_word, copy it. */ if (*end_word != '\0') { size_t end_off = strcspn(end_word + 1, delimiters); memmove(p, end_word, end_off + 1); p += end_off; /* p must correspond to end_word - 1 */ end_word += end_off + 1; } else { *p = '\0'; break; } } for (p++; p < end_word; p++) { *p = '\0'; } } if (*end_word == '\0') { *buf = end_word; } else { #if defined(GCC_DIAGNOSTIC) /* Disable spurious conversion warning for GCC */ # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wsign-conversion" #endif /* defined(GCC_DIAGNOSTIC) */ end_whitespace = end_word + strspn(&end_word[1], whitespace) + 1; #if defined(GCC_DIAGNOSTIC) # pragma GCC diagnostic pop #endif /* defined(GCC_DIAGNOSTIC) */ for (p = end_word; p < end_whitespace; p++) { *p = '\0'; } *buf = end_whitespace; } return begin_word; } /* Return HTTP header value, or NULL if not found. */ static const char * get_header(const struct mg_header *hdr, int num_hdr, const char *name) { int i; for (i = 0; i < num_hdr; i++) { if (!mg_strcasecmp(name, hdr[i].name)) { return hdr[i].value; } } return NULL; } /* Retrieve requested HTTP header multiple values, and return the number of * found occurrences */ static int get_req_headers(const struct mg_request_info *ri, const char *name, const char **output, int output_max_size) { int i; int cnt = 0; if (ri) { for (i = 0; i < ri->num_headers && cnt < output_max_size; i++) { if (!mg_strcasecmp(name, ri->http_headers[i].name)) { output[cnt++] = ri->http_headers[i].value; } } } return cnt; } CIVETWEB_API const char * mg_get_header(const struct mg_connection *conn, const char *name) { if (!conn) { return NULL; } if (conn->connection_type == CONNECTION_TYPE_REQUEST) { return get_header(conn->request_info.http_headers, conn->request_info.num_headers, name); } if (conn->connection_type == CONNECTION_TYPE_RESPONSE) { return get_header(conn->response_info.http_headers, conn->response_info.num_headers, name); } return NULL; } static const char * get_http_version(const struct mg_connection *conn) { if (!conn) { return NULL; } if (conn->connection_type == CONNECTION_TYPE_REQUEST) { return conn->request_info.http_version; } if (conn->connection_type == CONNECTION_TYPE_RESPONSE) { return conn->response_info.http_version; } return NULL; } /* A helper function for traversing a comma separated list of values. * It returns a list pointer shifted to the next value, or NULL if the end * of the list found. * Value is stored in val vector. If value has form "x=y", then eq_val * vector is initialized to point to the "y" part, and val vector length * is adjusted to point only to "x". */ static const char * next_option(const char *list, struct vec *val, struct vec *eq_val) { int end; reparse: if (val == NULL || list == NULL || *list == '\0') { /* End of the list */ return NULL; } /* Skip over leading LWS */ while (*list == ' ' || *list == '\t') list++; val->ptr = list; if ((list = strchr(val->ptr, ',')) != NULL) { /* Comma found. Store length and shift the list ptr */ val->len = ((size_t)(list - val->ptr)); list++; } else { /* This value is the last one */ list = val->ptr + strlen(val->ptr); val->len = ((size_t)(list - val->ptr)); } /* Adjust length for trailing LWS */ end = (int)val->len - 1; while (end >= 0 && ((val->ptr[end] == ' ') || (val->ptr[end] == '\t'))) end--; val->len = (size_t)(end) + (size_t)(1); if (val->len == 0) { /* Ignore any empty entries. */ goto reparse; } if (eq_val != NULL) { /* Value has form "x=y", adjust pointers and lengths * so that val points to "x", and eq_val points to "y". */ eq_val->len = 0; eq_val->ptr = (const char *)memchr(val->ptr, '=', val->len); if (eq_val->ptr != NULL) { eq_val->ptr++; /* Skip over '=' character */ eq_val->len = ((size_t)(val->ptr - eq_val->ptr)) + val->len; val->len = ((size_t)(eq_val->ptr - val->ptr)) - 1; } } return list; } /* A helper function for checking if a comma separated list of values * contains * the given option (case insensitvely). * 'header' can be NULL, in which case false is returned. */ static int header_has_option(const char *header, const char *option) { struct vec opt_vec; struct vec eq_vec; DEBUG_ASSERT(option != NULL); DEBUG_ASSERT(option[0] != '\0'); while ((header = next_option(header, &opt_vec, &eq_vec)) != NULL) { if (mg_strncasecmp(option, opt_vec.ptr, opt_vec.len) == 0) return 1; } return 0; } /* Sorting function implemented in a separate file */ #include "sort.h" /* Pattern matching has been reimplemented in a new file */ #include "match.h" /* HTTP 1.1 assumes keep alive if "Connection:" header is not set * This function must tolerate situations when connection info is not * set up, for example if request parsing failed. */ static int should_keep_alive(const struct mg_connection *conn) { const char *http_version; const char *header; /* First satisfy needs of the server */ if ((conn == NULL) || conn->must_close) { /* Close, if civetweb framework needs to close */ return 0; } if (mg_strcasecmp(conn->dom_ctx->config[ENABLE_KEEP_ALIVE], "yes") != 0) { /* Close, if keep alive is not enabled */ return 0; } /* Check explicit wish of the client */ header = mg_get_header(conn, "Connection"); if (header) { /* If there is a connection header from the client, obey */ if (header_has_option(header, "keep-alive")) { return 1; } return 0; } /* Use default of the standard */ http_version = get_http_version(conn); if (http_version && (0 == strcmp(http_version, "1.1"))) { /* HTTP 1.1 default is keep alive */ return 1; } /* HTTP 1.0 (and earlier) default is to close the connection */ return 0; } static int should_decode_url(const struct mg_connection *conn) { if (!conn || !conn->dom_ctx) { return 0; } return (mg_strcasecmp(conn->dom_ctx->config[DECODE_URL], "yes") == 0); } static int should_decode_query_string(const struct mg_connection *conn) { if (!conn || !conn->dom_ctx) { return 0; } return (mg_strcasecmp(conn->dom_ctx->config[DECODE_QUERY_STRING], "yes") == 0); } static const char * suggest_connection_header(const struct mg_connection *conn) { return should_keep_alive(conn) ? "keep-alive" : "close"; } #include "response.h" static void send_no_cache_header(struct mg_connection *conn) { /* Send all current and obsolete cache opt-out directives. */ mg_response_header_add(conn, "Cache-Control", "no-cache, no-store, " "must-revalidate, private, max-age=0", -1); mg_response_header_add(conn, "Expires", "0", -1); if (conn->protocol_type == PROTOCOL_TYPE_HTTP1) { /* Obsolete, but still send it for HTTP/1.0 */ mg_response_header_add(conn, "Pragma", "no-cache", -1); } } static void send_static_cache_header(struct mg_connection *conn) { #if !defined(NO_CACHING) int max_age; char val[64]; const char *cache_control = conn->dom_ctx->config[STATIC_FILE_CACHE_CONTROL]; /* If there is a full cache-control option configured,0 use it */ if (cache_control != NULL) { mg_response_header_add(conn, "Cache-Control", cache_control, -1); return; } /* Read the server config to check how long a file may be cached. * The configuration is in seconds. */ max_age = atoi(conn->dom_ctx->config[STATIC_FILE_MAX_AGE]); if (max_age <= 0) { /* 0 means "do not cache". All values <0 are reserved * and may be used differently in the future. */ /* If a file should not be cached, do not only send * max-age=0, but also pragmas and Expires headers. */ send_no_cache_header(conn); return; } /* Use "Cache-Control: max-age" instead of "Expires" header. * Reason: see https://www.mnot.net/blog/2007/05/15/expires_max-age */ /* See also https://www.mnot.net/cache_docs/ */ /* According to RFC 2616, Section 14.21, caching times should not exceed * one year. A year with 365 days corresponds to 31536000 seconds, a * leap * year to 31622400 seconds. For the moment, we just send whatever has * been configured, still the behavior for >1 year should be considered * as undefined. */ mg_snprintf( conn, NULL, val, sizeof(val), "max-age=%lu", (unsigned long)max_age); mg_response_header_add(conn, "Cache-Control", val, -1); #else /* NO_CACHING */ send_no_cache_header(conn); #endif /* !NO_CACHING */ } static void send_additional_header(struct mg_connection *conn) { const char *header = conn->dom_ctx->config[ADDITIONAL_HEADER]; #if !defined(NO_SSL) if (conn->dom_ctx->config[STRICT_HTTPS_MAX_AGE]) { long max_age = atol(conn->dom_ctx->config[STRICT_HTTPS_MAX_AGE]); if (max_age >= 0) { char val[64]; mg_snprintf(conn, NULL, val, sizeof(val), "max-age=%lu", (unsigned long)max_age); mg_response_header_add(conn, "Strict-Transport-Security", val, -1); } } #endif if (header && header[0]) { mg_response_header_add_lines(conn, header); } } static void send_cors_header(struct mg_connection *conn) { const char *origin_hdr = mg_get_header(conn, "Origin"); const char *cors_orig_cfg = conn->dom_ctx->config[ACCESS_CONTROL_ALLOW_ORIGIN]; if (cors_orig_cfg && *cors_orig_cfg && origin_hdr && *origin_hdr) { /* Cross-origin resource sharing (CORS), see * http://www.html5rocks.com/en/tutorials/cors/, * http://www.html5rocks.com/static/images/cors_server_flowchart.png * CORS preflight is not supported for files. */ mg_response_header_add(conn, "Access-Control-Allow-Origin", cors_orig_cfg, -1); } } #if !defined(NO_FILESYSTEMS) static void handle_file_based_request(struct mg_connection *conn, const char *path, struct mg_file *filep); #endif /* NO_FILESYSTEMS */ CIVETWEB_API const char * mg_get_response_code_text(const struct mg_connection *conn, int response_code) { /* See IANA HTTP status code assignment: * http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml */ switch (response_code) { /* RFC2616 Section 10.1 - Informational 1xx */ case 100: return "Continue"; /* RFC2616 Section 10.1.1 */ case 101: return "Switching Protocols"; /* RFC2616 Section 10.1.2 */ case 102: return "Processing"; /* RFC2518 Section 10.1 */ /* RFC2616 Section 10.2 - Successful 2xx */ case 200: return "OK"; /* RFC2616 Section 10.2.1 */ case 201: return "Created"; /* RFC2616 Section 10.2.2 */ case 202: return "Accepted"; /* RFC2616 Section 10.2.3 */ case 203: return "Non-Authoritative Information"; /* RFC2616 Section 10.2.4 */ case 204: return "No Content"; /* RFC2616 Section 10.2.5 */ case 205: return "Reset Content"; /* RFC2616 Section 10.2.6 */ case 206: return "Partial Content"; /* RFC2616 Section 10.2.7 */ case 207: return "Multi-Status"; /* RFC2518 Section 10.2, RFC4918 Section 11.1 */ case 208: return "Already Reported"; /* RFC5842 Section 7.1 */ case 226: return "IM used"; /* RFC3229 Section 10.4.1 */ /* RFC2616 Section 10.3 - Redirection 3xx */ case 300: return "Multiple Choices"; /* RFC2616 Section 10.3.1 */ case 301: return "Moved Permanently"; /* RFC2616 Section 10.3.2 */ case 302: return "Found"; /* RFC2616 Section 10.3.3 */ case 303: return "See Other"; /* RFC2616 Section 10.3.4 */ case 304: return "Not Modified"; /* RFC2616 Section 10.3.5 */ case 305: return "Use Proxy"; /* RFC2616 Section 10.3.6 */ case 307: return "Temporary Redirect"; /* RFC2616 Section 10.3.8 */ case 308: return "Permanent Redirect"; /* RFC7238 Section 3 */ /* RFC2616 Section 10.4 - Client Error 4xx */ case 400: return "Bad Request"; /* RFC2616 Section 10.4.1 */ case 401: return "Unauthorized"; /* RFC2616 Section 10.4.2 */ case 402: return "Payment Required"; /* RFC2616 Section 10.4.3 */ case 403: return "Forbidden"; /* RFC2616 Section 10.4.4 */ case 404: return "Not Found"; /* RFC2616 Section 10.4.5 */ case 405: return "Method Not Allowed"; /* RFC2616 Section 10.4.6 */ case 406: return "Not Acceptable"; /* RFC2616 Section 10.4.7 */ case 407: return "Proxy Authentication Required"; /* RFC2616 Section 10.4.8 */ case 408: return "Request Time-out"; /* RFC2616 Section 10.4.9 */ case 409: return "Conflict"; /* RFC2616 Section 10.4.10 */ case 410: return "Gone"; /* RFC2616 Section 10.4.11 */ case 411: return "Length Required"; /* RFC2616 Section 10.4.12 */ case 412: return "Precondition Failed"; /* RFC2616 Section 10.4.13 */ case 413: return "Request Entity Too Large"; /* RFC2616 Section 10.4.14 */ case 414: return "Request-URI Too Large"; /* RFC2616 Section 10.4.15 */ case 415: return "Unsupported Media Type"; /* RFC2616 Section 10.4.16 */ case 416: return "Requested range not satisfiable"; /* RFC2616 Section 10.4.17 */ case 417: return "Expectation Failed"; /* RFC2616 Section 10.4.18 */ case 421: return "Misdirected Request"; /* RFC7540 Section 9.1.2 */ case 422: return "Unproccessable entity"; /* RFC2518 Section 10.3, RFC4918 * Section 11.2 */ case 423: return "Locked"; /* RFC2518 Section 10.4, RFC4918 Section 11.3 */ case 424: return "Failed Dependency"; /* RFC2518 Section 10.5, RFC4918 * Section 11.4 */ case 426: return "Upgrade Required"; /* RFC 2817 Section 4 */ case 428: return "Precondition Required"; /* RFC 6585, Section 3 */ case 429: return "Too Many Requests"; /* RFC 6585, Section 4 */ case 431: return "Request Header Fields Too Large"; /* RFC 6585, Section 5 */ case 451: return "Unavailable For Legal Reasons"; /* draft-tbray-http-legally-restricted-status-05, * Section 3 */ /* RFC2616 Section 10.5 - Server Error 5xx */ case 500: return "Internal Server Error"; /* RFC2616 Section 10.5.1 */ case 501: return "Not Implemented"; /* RFC2616 Section 10.5.2 */ case 502: return "Bad Gateway"; /* RFC2616 Section 10.5.3 */ case 503: return "Service Unavailable"; /* RFC2616 Section 10.5.4 */ case 504: return "Gateway Time-out"; /* RFC2616 Section 10.5.5 */ case 505: return "HTTP Version not supported"; /* RFC2616 Section 10.5.6 */ case 506: return "Variant Also Negotiates"; /* RFC 2295, Section 8.1 */ case 507: return "Insufficient Storage"; /* RFC2518 Section 10.6, RFC4918 * Section 11.5 */ case 508: return "Loop Detected"; /* RFC5842 Section 7.1 */ case 510: return "Not Extended"; /* RFC 2774, Section 7 */ case 511: return "Network Authentication Required"; /* RFC 6585, Section 6 */ /* Other status codes, not shown in the IANA HTTP status code * assignment. * E.g., "de facto" standards due to common use, ... */ case 418: return "I am a teapot"; /* RFC2324 Section 2.3.2 */ case 419: return "Authentication Timeout"; /* common use */ case 420: return "Enhance Your Calm"; /* common use */ case 440: return "Login Timeout"; /* common use */ case 509: return "Bandwidth Limit Exceeded"; /* common use */ default: /* This error code is unknown. This should not happen. */ if (conn) { mg_cry_internal(conn, "Unknown HTTP response code: %u", response_code); } /* Return at least a category according to RFC 2616 Section 10. */ if (response_code >= 100 && response_code < 200) { /* Unknown informational status code */ return "Information"; } if (response_code >= 200 && response_code < 300) { /* Unknown success code */ return "Success"; } if (response_code >= 300 && response_code < 400) { /* Unknown redirection code */ return "Redirection"; } if (response_code >= 400 && response_code < 500) { /* Unknown request error code */ return "Client Error"; } if (response_code >= 500 && response_code < 600) { /* Unknown server error code */ return "Server Error"; } /* Response code not even within reasonable range */ return ""; } } static int mg_send_http_error_impl(struct mg_connection *conn, int status, const char *fmt, va_list args) { char errmsg_buf[MG_BUF_LEN]; va_list ap; int has_body; #if !defined(NO_FILESYSTEMS) char path_buf[UTF8_PATH_MAX]; int len, i, page_handler_found, scope, truncated; const char *error_handler = NULL; struct mg_file error_page_file = STRUCT_FILE_INITIALIZER; const char *error_page_file_ext, *tstr; #endif /* NO_FILESYSTEMS */ int handled_by_callback = 0; if ((conn == NULL) || (fmt == NULL)) { return -2; } /* Set status (for log) */ conn->status_code = status; /* Errors 1xx, 204 and 304 MUST NOT send a body */ has_body = ((status > 199) && (status != 204) && (status != 304)); /* Prepare message in buf, if required */ if (has_body || (!conn->in_error_handler && (conn->phys_ctx->callbacks.http_error != NULL))) { /* Store error message in errmsg_buf */ va_copy(ap, args); mg_vsnprintf(conn, NULL, errmsg_buf, sizeof(errmsg_buf), fmt, ap); va_end(ap); /* In a debug build, print all html errors */ DEBUG_TRACE("Error %i - [%s]", status, errmsg_buf); } /* If there is a http_error callback, call it. * But don't do it recursively, if callback calls mg_send_http_error again. */ if (!conn->in_error_handler && (conn->phys_ctx->callbacks.http_error != NULL)) { /* Mark in_error_handler to avoid recursion and call user callback. */ conn->in_error_handler = 1; handled_by_callback = (conn->phys_ctx->callbacks.http_error(conn, status, errmsg_buf) == 0); conn->in_error_handler = 0; } if (!handled_by_callback) { /* Check for recursion */ if (conn->in_error_handler) { DEBUG_TRACE( "Recursion when handling error %u - fall back to default", status); #if !defined(NO_FILESYSTEMS) } else { /* Send user defined error pages, if defined */ error_handler = conn->dom_ctx->config[ERROR_PAGES]; error_page_file_ext = conn->dom_ctx->config[INDEX_FILES]; page_handler_found = 0; if (error_handler != NULL) { for (scope = 1; (scope <= 3) && !page_handler_found; scope++) { switch (scope) { case 1: /* Handler for specific error, e.g. 404 error */ mg_snprintf(conn, &truncated, path_buf, sizeof(path_buf) - 32, "%serror%03u.", error_handler, status); break; case 2: /* Handler for error group, e.g., 5xx error * handler * for all server errors (500-599) */ mg_snprintf(conn, &truncated, path_buf, sizeof(path_buf) - 32, "%serror%01uxx.", error_handler, status / 100); break; default: /* Handler for all errors */ mg_snprintf(conn, &truncated, path_buf, sizeof(path_buf) - 32, "%serror.", error_handler); break; } /* String truncation in buf may only occur if * error_handler is too long. This string is * from the config, not from a client. */ (void)truncated; /* The following code is redundant, but it should avoid * false positives in static source code analyzers and * vulnerability scanners. */ path_buf[sizeof(path_buf) - 32] = 0; len = (int)strlen(path_buf); if (len > (int)sizeof(path_buf) - 32) { len = (int)sizeof(path_buf) - 32; } /* Start with the file extension from the configuration. */ tstr = strchr(error_page_file_ext, '.'); while (tstr) { for (i = 1; (i < 32) && (tstr[i] != 0) && (tstr[i] != ','); i++) { /* buffer overrun is not possible here, since * (i < 32) && (len < sizeof(path_buf) - 32) * ==> (i + len) < sizeof(path_buf) */ path_buf[len + i - 1] = tstr[i]; } /* buffer overrun is not possible here, since * (i <= 32) && (len < sizeof(path_buf) - 32) * ==> (i + len) <= sizeof(path_buf) */ path_buf[len + i - 1] = 0; if (mg_stat(conn, path_buf, &error_page_file.stat)) { DEBUG_TRACE("Check error page %s - found", path_buf); page_handler_found = 1; break; } DEBUG_TRACE("Check error page %s - not found", path_buf); /* Continue with the next file extension from the * configuration (if there is a next one). */ tstr = strchr(tstr + i, '.'); } } } if (page_handler_found) { conn->in_error_handler = 1; handle_file_based_request(conn, path_buf, &error_page_file); conn->in_error_handler = 0; return 0; } #endif /* NO_FILESYSTEMS */ } /* No custom error page. Send default error page. */ conn->must_close = 1; mg_response_header_start(conn, status); send_no_cache_header(conn); send_additional_header(conn); send_cors_header(conn); if (has_body) { mg_response_header_add(conn, "Content-Type", "text/plain; charset=utf-8", -1); } mg_response_header_send(conn); /* HTTP responses 1xx, 204 and 304 MUST NOT send a body */ if (has_body) { /* For other errors, send a generic error message. */ const char *status_text = mg_get_response_code_text(conn, status); mg_printf(conn, "Error %d: %s\n", status, status_text); mg_write(conn, errmsg_buf, strlen(errmsg_buf)); } else { /* No body allowed. Close the connection. */ DEBUG_TRACE("Error %i", status); } } return 0; } CIVETWEB_API int mg_send_http_error(struct mg_connection *conn, int status, const char *fmt, ...) { va_list ap; int ret; va_start(ap, fmt); ret = mg_send_http_error_impl(conn, status, fmt, ap); va_end(ap); return ret; } CIVETWEB_API int mg_send_http_ok(struct mg_connection *conn, const char *mime_type, long long content_length) { if ((mime_type == NULL) || (*mime_type == 0)) { /* No content type defined: default to text/html */ mime_type = "text/html"; } mg_response_header_start(conn, 200); send_no_cache_header(conn); send_additional_header(conn); send_cors_header(conn); mg_response_header_add(conn, "Content-Type", mime_type, -1); if (content_length < 0) { /* Size not known. Use chunked encoding (HTTP/1.x) */ if (conn->protocol_type == PROTOCOL_TYPE_HTTP1) { /* Only HTTP/1.x defines "chunked" encoding, HTTP/2 does not*/ mg_response_header_add(conn, "Transfer-Encoding", "chunked", -1); } } else { char len[32]; int trunc = 0; mg_snprintf(conn, &trunc, len, sizeof(len), "%" UINT64_FMT, (uint64_t)content_length); if (!trunc) { /* Since 32 bytes is enough to hold any 64 bit decimal number, * !trunc is always true */ mg_response_header_add(conn, "Content-Length", len, -1); } } mg_response_header_send(conn); return 0; } CIVETWEB_API int mg_send_http_redirect(struct mg_connection *conn, const char *target_url, int redirect_code) { /* Send a 30x redirect response. * * Redirect types (status codes): * * Status | Perm/Temp | Method | Version * 301 | permanent | POST->GET undefined | HTTP/1.0 * 302 | temporary | POST->GET undefined | HTTP/1.0 * 303 | temporary | always use GET | HTTP/1.1 * 307 | temporary | always keep method | HTTP/1.1 * 308 | permanent | always keep method | HTTP/1.1 */ #if defined(MG_SEND_REDIRECT_BODY) char redirect_body[MG_BUF_LEN]; size_t content_len = 0; char content_len_text[32]; #endif /* In case redirect_code=0, use 307. */ if (redirect_code == 0) { redirect_code = 307; } /* In case redirect_code is none of the above, return error. */ if ((redirect_code != 301) && (redirect_code != 302) && (redirect_code != 303) && (redirect_code != 307) && (redirect_code != 308)) { /* Parameter error */ return -2; } /* If target_url is not defined, redirect to "/". */ if ((target_url == NULL) || (*target_url == 0)) { target_url = "/"; } #if defined(MG_SEND_REDIRECT_BODY) /* TODO: condition name? */ /* Prepare a response body with a hyperlink. * * According to RFC2616 (and RFC1945 before): * Unless the request method was HEAD, the entity of the * response SHOULD contain a short hypertext note with a hyperlink to * the new URI(s). * * However, this response body is not useful in M2M communication. * Probably the original reason in the RFC was, clients not supporting * a 30x HTTP redirect could still show the HTML page and let the user * press the link. Since current browsers support 30x HTTP, the additional * HTML body does not seem to make sense anymore. * * The new RFC7231 (Section 6.4) does no longer recommend it ("SHOULD"), * but it only notes: * The server's response payload usually contains a short * hypertext note with a hyperlink to the new URI(s). * * Deactivated by default. If you need the 30x body, set the define. */ mg_snprintf( conn, NULL /* ignore truncation */, redirect_body, sizeof(redirect_body), "%s%s", redirect_text, target_url, target_url); content_len = strlen(reply); sprintf(content_len_text, "%lu", (unsigned long)content_len); #endif /* Send all required headers */ mg_response_header_start(conn, redirect_code); mg_response_header_add(conn, "Location", target_url, -1); if ((redirect_code == 301) || (redirect_code == 308)) { /* Permanent redirect */ send_static_cache_header(conn); } else { /* Temporary redirect */ send_no_cache_header(conn); } send_additional_header(conn); send_cors_header(conn); #if defined(MG_SEND_REDIRECT_BODY) mg_response_header_add(conn, "Content-Type", "text/html", -1); mg_response_header_add(conn, "Content-Length", content_len_text, -1); #else mg_response_header_add(conn, "Content-Length", "0", 1); #endif mg_response_header_send(conn); #if defined(MG_SEND_REDIRECT_BODY) /* Send response body */ /* ... unless it is a HEAD request */ if (0 != strcmp(conn->request_info.request_method, "HEAD")) { ret = mg_write(conn, redirect_body, content_len); } #endif return 1; } #if defined(_WIN32) /* Create substitutes for POSIX functions in Win32. */ #if defined(GCC_DIAGNOSTIC) /* Show no warning in case system functions are not used. */ # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wunused-function" #endif static int pthread_mutex_init(pthread_mutex_t *mutex, void *unused) { (void)unused; /* Always initialize as PTHREAD_MUTEX_RECURSIVE */ InitializeCriticalSection(&mutex->sec); return 0; } static int pthread_mutex_destroy(pthread_mutex_t *mutex) { DeleteCriticalSection(&mutex->sec); return 0; } static int pthread_mutex_lock(pthread_mutex_t *mutex) { EnterCriticalSection(&mutex->sec); return 0; } static int pthread_mutex_unlock(pthread_mutex_t *mutex) { LeaveCriticalSection(&mutex->sec); return 0; } FUNCTION_MAY_BE_UNUSED static int pthread_cond_init(pthread_cond_t *cv, const void *unused) { (void)unused; (void)pthread_mutex_init(&cv->threadIdSec, &pthread_mutex_attr); cv->waiting_thread = NULL; return 0; } FUNCTION_MAY_BE_UNUSED static int pthread_cond_timedwait(pthread_cond_t *cv, pthread_mutex_t *mutex, FUNCTION_MAY_BE_UNUSED const struct timespec *abstime) { struct mg_workerTLS **ptls, *tls = (struct mg_workerTLS *)pthread_getspecific(sTlsKey); int ok; uint64_t nsnow, nswaitabs; int64_t nswaitrel; DWORD mswaitrel; pthread_mutex_lock(&cv->threadIdSec); /* Add this thread to cv's waiting list */ ptls = &cv->waiting_thread; for (; *ptls != NULL; ptls = &(*ptls)->next_waiting_thread) ; tls->next_waiting_thread = NULL; *ptls = tls; pthread_mutex_unlock(&cv->threadIdSec); if (abstime) { nsnow = mg_get_current_time_ns(); nswaitabs = (((uint64_t)abstime->tv_sec) * 1000000000) + abstime->tv_nsec; nswaitrel = nswaitabs - nsnow; if (nswaitrel < 0) { nswaitrel = 0; } mswaitrel = (DWORD)(nswaitrel / 1000000); } else { mswaitrel = (DWORD)INFINITE; } pthread_mutex_unlock(mutex); ok = (WAIT_OBJECT_0 == WaitForSingleObject(tls->pthread_cond_helper_mutex, mswaitrel)); if (!ok) { ok = 1; pthread_mutex_lock(&cv->threadIdSec); ptls = &cv->waiting_thread; for (; *ptls != NULL; ptls = &(*ptls)->next_waiting_thread) { if (*ptls == tls) { *ptls = tls->next_waiting_thread; ok = 0; break; } } pthread_mutex_unlock(&cv->threadIdSec); if (ok) { WaitForSingleObject(tls->pthread_cond_helper_mutex, (DWORD)INFINITE); } } /* This thread has been removed from cv's waiting list */ pthread_mutex_lock(mutex); return ok ? 0 : -1; } FUNCTION_MAY_BE_UNUSED static int pthread_cond_wait(pthread_cond_t *cv, pthread_mutex_t *mutex) { return pthread_cond_timedwait(cv, mutex, NULL); } FUNCTION_MAY_BE_UNUSED static int pthread_cond_signal(pthread_cond_t *cv) { HANDLE wkup = NULL; BOOL ok = FALSE; pthread_mutex_lock(&cv->threadIdSec); if (cv->waiting_thread) { wkup = cv->waiting_thread->pthread_cond_helper_mutex; cv->waiting_thread = cv->waiting_thread->next_waiting_thread; ok = SetEvent(wkup); DEBUG_ASSERT(ok); } pthread_mutex_unlock(&cv->threadIdSec); return ok ? 0 : 1; } FUNCTION_MAY_BE_UNUSED static int pthread_cond_broadcast(pthread_cond_t *cv) { pthread_mutex_lock(&cv->threadIdSec); while (cv->waiting_thread) { pthread_cond_signal(cv); } pthread_mutex_unlock(&cv->threadIdSec); return 0; } FUNCTION_MAY_BE_UNUSED static int pthread_cond_destroy(pthread_cond_t *cv) { pthread_mutex_lock(&cv->threadIdSec); DEBUG_ASSERT(cv->waiting_thread == NULL); pthread_mutex_unlock(&cv->threadIdSec); pthread_mutex_destroy(&cv->threadIdSec); return 0; } #if defined(ALTERNATIVE_QUEUE) FUNCTION_MAY_BE_UNUSED static void * event_create(void) { return (void *)CreateEvent(NULL, FALSE, FALSE, NULL); } FUNCTION_MAY_BE_UNUSED static int event_wait(void *eventhdl) { int res = WaitForSingleObject((HANDLE)eventhdl, (DWORD)INFINITE); return (res == WAIT_OBJECT_0); } FUNCTION_MAY_BE_UNUSED static int event_signal(void *eventhdl) { return (int)SetEvent((HANDLE)eventhdl); } FUNCTION_MAY_BE_UNUSED static void event_destroy(void *eventhdl) { CloseHandle((HANDLE)eventhdl); } #endif #if defined(GCC_DIAGNOSTIC) /* Enable unused function warning again */ # pragma GCC diagnostic pop #endif /* For Windows, change all slashes to backslashes in path names. */ static void change_slashes_to_backslashes(char *path) { int i; for (i = 0; path[i] != '\0'; i++) { if (path[i] == '/') { path[i] = '\\'; } /* remove double backslash (check i > 0 to preserve UNC paths, * like \\server\file.txt) */ if ((i > 0) && (path[i] == '\\')) { while ((path[i + 1] == '\\') || (path[i + 1] == '/')) { (void)memmove(path + i + 1, path + i + 2, strlen(path + i + 1)); } } } } static int mg_wcscasecmp(const wchar_t *s1, const wchar_t *s2) { int diff; do { diff = ((*s1 >= L'A') && (*s1 <= L'Z') ? (*s1 - L'A' + L'a') : *s1) - ((*s2 >= L'A') && (*s2 <= L'Z') ? (*s2 - L'A' + L'a') : *s2); s1++; s2++; } while ((diff == 0) && (s1[-1] != L'\0')); return diff; } /* Encode 'path' which is assumed UTF-8 string, into UNICODE string. * wbuf and wbuf_len is a target buffer and its length. */ static void path_to_unicode(const struct mg_connection *conn, const char *path, wchar_t *wbuf, size_t wbuf_len) { char buf[UTF8_PATH_MAX], buf2[UTF8_PATH_MAX]; wchar_t wbuf2[UTF16_PATH_MAX + 1]; DWORD long_len, err; int (*fcompare)(const wchar_t *, const wchar_t *) = mg_wcscasecmp; mg_strlcpy(buf, path, sizeof(buf)); change_slashes_to_backslashes(buf); /* Convert to Unicode and back. If doubly-converted string does not * match the original, something is fishy, reject. */ memset(wbuf, 0, wbuf_len * sizeof(wchar_t)); MultiByteToWideChar(CP_UTF8, 0, buf, -1, wbuf, (int)wbuf_len); WideCharToMultiByte( CP_UTF8, 0, wbuf, (int)wbuf_len, buf2, sizeof(buf2), NULL, NULL); if (strcmp(buf, buf2) != 0) { wbuf[0] = L'\0'; } /* Windows file systems are not case sensitive, but you can still use * uppercase and lowercase letters (on all modern file systems). * The server can check if the URI uses the same upper/lowercase * letters an the file system, effectively making Windows servers * case sensitive (like Linux servers are). It is still not possible * to use two files with the same name in different cases on Windows * (like /a and /A) - this would be possible in Linux. * As a default, Windows is not case sensitive, but the case sensitive * file name check can be activated by an additional configuration. */ if (conn) { if (conn->dom_ctx->config[CASE_SENSITIVE_FILES] && !mg_strcasecmp(conn->dom_ctx->config[CASE_SENSITIVE_FILES], "yes")) { /* Use case sensitive compare function */ fcompare = wcscmp; } } (void)conn; /* conn is currently unused */ /* Only accept a full file path, not a Windows short (8.3) path. */ memset(wbuf2, 0, ARRAY_SIZE(wbuf2) * sizeof(wchar_t)); long_len = GetLongPathNameW(wbuf, wbuf2, ARRAY_SIZE(wbuf2) - 1); if (long_len == 0) { err = GetLastError(); if (err == ERROR_FILE_NOT_FOUND) { /* File does not exist. This is not always a problem here. */ return; } } if ((long_len >= ARRAY_SIZE(wbuf2)) || (fcompare(wbuf, wbuf2) != 0)) { /* Short name is used. */ wbuf[0] = L'\0'; } } #if !defined(NO_FILESYSTEMS) /* Get file information, return 1 if file exists, 0 if not */ static int mg_stat(const struct mg_connection *conn, const char *path, struct mg_file_stat *filep) { wchar_t wbuf[UTF16_PATH_MAX]; WIN32_FILE_ATTRIBUTE_DATA info; time_t creation_time; size_t len; if (!filep) { return 0; } memset(filep, 0, sizeof(*filep)); if (mg_path_suspicious(conn, path)) { return 0; } path_to_unicode(conn, path, wbuf, ARRAY_SIZE(wbuf)); /* Windows happily opens files with some garbage at the end of file name. * For example, fopen("a.cgi ", "r") on Windows successfully opens * "a.cgi", despite one would expect an error back. */ len = strlen(path); if ((len > 0) && (path[len - 1] != ' ') && (path[len - 1] != '.') && (GetFileAttributesExW(wbuf, GetFileExInfoStandard, &info) != 0)) { filep->size = MAKEUQUAD(info.nFileSizeLow, info.nFileSizeHigh); filep->last_modified = SYS2UNIX_TIME(info.ftLastWriteTime.dwLowDateTime, info.ftLastWriteTime.dwHighDateTime); /* On Windows, the file creation time can be higher than the * modification time, e.g. when a file is copied. * Since the Last-Modified timestamp is used for caching * it should be based on the most recent timestamp. */ creation_time = SYS2UNIX_TIME(info.ftCreationTime.dwLowDateTime, info.ftCreationTime.dwHighDateTime); if (creation_time > filep->last_modified) { filep->last_modified = creation_time; } filep->is_directory = info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY; return 1; } return 0; } #endif static int mg_remove(const struct mg_connection *conn, const char *path) { wchar_t wbuf[UTF16_PATH_MAX]; path_to_unicode(conn, path, wbuf, ARRAY_SIZE(wbuf)); return DeleteFileW(wbuf) ? 0 : -1; } static int mg_mkdir(const struct mg_connection *conn, const char *path, int mode) { wchar_t wbuf[UTF16_PATH_MAX]; (void)mode; path_to_unicode(conn, path, wbuf, ARRAY_SIZE(wbuf)); return CreateDirectoryW(wbuf, NULL) ? 0 : -1; } /* Create substitutes for POSIX functions in Win32. */ #if defined(GCC_DIAGNOSTIC) /* Show no warning in case system functions are not used. */ # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wunused-function" #endif /* Implementation of POSIX opendir/closedir/readdir for Windows. */ FUNCTION_MAY_BE_UNUSED static DIR * mg_opendir(const struct mg_connection *conn, const char *name) { DIR *dir = NULL; wchar_t wpath[UTF16_PATH_MAX]; DWORD attrs; if (name == NULL) { SetLastError(ERROR_BAD_ARGUMENTS); } else if ((dir = (DIR *)mg_malloc(sizeof(*dir))) == NULL) { SetLastError(ERROR_NOT_ENOUGH_MEMORY); } else { path_to_unicode(conn, name, wpath, ARRAY_SIZE(wpath)); attrs = GetFileAttributesW(wpath); if ((wcslen(wpath) + 2 < ARRAY_SIZE(wpath)) && (attrs != 0xFFFFFFFF) && ((attrs & FILE_ATTRIBUTE_DIRECTORY) != 0)) { (void)wcscat(wpath, L"\\*"); dir->handle = FindFirstFileW(wpath, &dir->info); dir->result.d_name[0] = '\0'; } else { mg_free(dir); dir = NULL; } } return dir; } FUNCTION_MAY_BE_UNUSED static int mg_closedir(DIR *dir) { int result = 0; if (dir != NULL) { if (dir->handle != INVALID_HANDLE_VALUE) result = FindClose(dir->handle) ? 0 : -1; mg_free(dir); } else { result = -1; SetLastError(ERROR_BAD_ARGUMENTS); } return result; } FUNCTION_MAY_BE_UNUSED static struct dirent * mg_readdir(DIR *dir) { struct dirent *result = 0; if (dir) { if (dir->handle != INVALID_HANDLE_VALUE) { result = &dir->result; (void)WideCharToMultiByte(CP_UTF8, 0, dir->info.cFileName, -1, result->d_name, sizeof(result->d_name), NULL, NULL); if (!FindNextFileW(dir->handle, &dir->info)) { (void)FindClose(dir->handle); dir->handle = INVALID_HANDLE_VALUE; } } else { SetLastError(ERROR_FILE_NOT_FOUND); } } else { SetLastError(ERROR_BAD_ARGUMENTS); } return result; } #if !defined(HAVE_POLL) #undef POLLIN #undef POLLPRI #undef POLLOUT #undef POLLERR #define POLLIN (1) /* Data ready - read will not block. */ #define POLLPRI (2) /* Priority data ready. */ #define POLLOUT (4) /* Send queue not full - write will not block. */ #define POLLERR (8) /* Error event */ FUNCTION_MAY_BE_UNUSED static int poll(struct mg_pollfd *pfd, unsigned int n, int milliseconds) { struct timeval tv; fd_set rset; fd_set wset; fd_set eset; unsigned int i; int result; SOCKET maxfd = 0; memset(&tv, 0, sizeof(tv)); tv.tv_sec = milliseconds / 1000; tv.tv_usec = (milliseconds % 1000) * 1000; FD_ZERO(&rset); FD_ZERO(&wset); FD_ZERO(&eset); for (i = 0; i < n; i++) { if (pfd[i].events & (POLLIN | POLLOUT | POLLERR)) { if (pfd[i].events & POLLIN) { FD_SET(pfd[i].fd, &rset); } if (pfd[i].events & POLLOUT) { FD_SET(pfd[i].fd, &wset); } /* Check for errors for any FD in the set */ FD_SET(pfd[i].fd, &eset); } pfd[i].revents = 0; if (pfd[i].fd > maxfd) { maxfd = pfd[i].fd; } } if ((result = select((int)maxfd + 1, &rset, &wset, &eset, &tv)) > 0) { for (i = 0; i < n; i++) { if (FD_ISSET(pfd[i].fd, &rset)) { pfd[i].revents |= POLLIN; } if (FD_ISSET(pfd[i].fd, &wset)) { pfd[i].revents |= POLLOUT; } if (FD_ISSET(pfd[i].fd, &eset)) { pfd[i].revents |= POLLERR; } } } /* We should subtract the time used in select from remaining * "milliseconds", in particular if called from mg_poll with a * timeout quantum. * Unfortunately, the remaining time is not stored in "tv" in all * implementations, so the result in "tv" must be considered undefined. * See http://man7.org/linux/man-pages/man2/select.2.html */ return result; } #endif /* HAVE_POLL */ #if defined(GCC_DIAGNOSTIC) /* Enable unused function warning again */ # pragma GCC diagnostic pop #endif static void set_close_on_exec(SOCKET sock, const struct mg_connection *conn /* may be null */, struct mg_context *ctx /* may be null */) { (void)conn; /* Unused. */ (void)ctx; (void)SetHandleInformation((HANDLE)(intptr_t)sock, HANDLE_FLAG_INHERIT, 0); } CIVETWEB_API int mg_start_thread(mg_thread_func_t f, void *p) { #if defined(USE_STACK_SIZE) && (USE_STACK_SIZE > 1) /* Compile-time option to control stack size, e.g. * -DUSE_STACK_SIZE=16384 */ return ((_beginthread((void(__cdecl *)(void *))f, USE_STACK_SIZE, p) == ((uintptr_t)(-1L))) ? -1 : 0); #else return ( (_beginthread((void(__cdecl *)(void *))f, 0, p) == ((uintptr_t)(-1L))) ? -1 : 0); #endif /* defined(USE_STACK_SIZE) && (USE_STACK_SIZE > 1) */ } /* Start a thread storing the thread context. */ static int mg_start_thread_with_id(unsigned(__stdcall *f)(void *), void *p, pthread_t *threadidptr) { uintptr_t uip; HANDLE threadhandle; int result = -1; uip = _beginthreadex(NULL, 0, f, p, 0, NULL); threadhandle = (HANDLE)uip; if ((uip != 0) && (threadidptr != NULL)) { *threadidptr = threadhandle; result = 0; } return result; } /* Wait for a thread to finish. */ static int mg_join_thread(pthread_t threadid) { int result; DWORD dwevent; result = -1; dwevent = WaitForSingleObject(threadid, (DWORD)INFINITE); if (dwevent == WAIT_FAILED) { DEBUG_TRACE("WaitForSingleObject() failed, error %d", ERRNO); } else { if (dwevent == WAIT_OBJECT_0) { CloseHandle(threadid); result = 0; } } return result; } #if !defined(NO_SSL_DL) && !defined(NO_SSL) /* If SSL is loaded dynamically, dlopen/dlclose is required. */ /* Create substitutes for POSIX functions in Win32. */ #if defined(GCC_DIAGNOSTIC) /* Show no warning in case system functions are not used. */ # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wunused-function" #endif FUNCTION_MAY_BE_UNUSED static HANDLE dlopen(const char *dll_name, int flags) { wchar_t wbuf[UTF16_PATH_MAX]; (void)flags; path_to_unicode(NULL, dll_name, wbuf, ARRAY_SIZE(wbuf)); return LoadLibraryW(wbuf); } FUNCTION_MAY_BE_UNUSED static int dlclose(void *handle) { int result; if (FreeLibrary((HMODULE)handle) != 0) { result = 0; } else { result = -1; } return result; } #if defined(GCC_DIAGNOSTIC) /* Enable unused function warning again */ # pragma GCC diagnostic pop #endif #endif #if !defined(NO_CGI) #define SIGKILL (0) static int kill(pid_t pid, int sig_num) { (void)TerminateProcess((HANDLE)pid, (UINT)sig_num); (void)CloseHandle((HANDLE)pid); return 0; } #if !defined(WNOHANG) #define WNOHANG (1) #endif static pid_t waitpid(pid_t pid, int *status, int flags) { DWORD timeout = INFINITE; DWORD waitres; (void)status; /* Currently not used by any client here */ if ((flags | WNOHANG) == WNOHANG) { timeout = 0; } waitres = WaitForSingleObject((HANDLE)pid, timeout); if (waitres == WAIT_OBJECT_0) { return pid; } if (waitres == WAIT_TIMEOUT) { return 0; } return (pid_t)-1; } static void trim_trailing_whitespaces(char *s) { char *e = s + strlen(s); while ((e > s) && isspace((unsigned char)e[-1])) { *(--e) = '\0'; } } static pid_t spawn_process(struct mg_connection *conn, const char *prog, char *envblk, char *envp[], int fdin[2], int fdout[2], int fderr[2], const char *dir, int cgi_config_idx) { HANDLE me; char *interp; char *interp_arg = 0; char full_dir[UTF8_PATH_MAX], cmdline[UTF8_PATH_MAX], buf[UTF8_PATH_MAX]; int truncated; struct mg_file file = STRUCT_FILE_INITIALIZER; STARTUPINFOA si; PROCESS_INFORMATION pi = {0}; (void)envp; memset(&si, 0, sizeof(si)); si.cb = sizeof(si); si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW; si.wShowWindow = SW_HIDE; me = GetCurrentProcess(); DuplicateHandle(me, (HANDLE)_get_osfhandle(fdin[0]), me, &si.hStdInput, 0, TRUE, DUPLICATE_SAME_ACCESS); DuplicateHandle(me, (HANDLE)_get_osfhandle(fdout[1]), me, &si.hStdOutput, 0, TRUE, DUPLICATE_SAME_ACCESS); DuplicateHandle(me, (HANDLE)_get_osfhandle(fderr[1]), me, &si.hStdError, 0, TRUE, DUPLICATE_SAME_ACCESS); /* Mark handles that should not be inherited. See * https://msdn.microsoft.com/en-us/library/windows/desktop/ms682499%28v=vs.85%29.aspx */ SetHandleInformation((HANDLE)_get_osfhandle(fdin[1]), HANDLE_FLAG_INHERIT, 0); SetHandleInformation((HANDLE)_get_osfhandle(fdout[0]), HANDLE_FLAG_INHERIT, 0); SetHandleInformation((HANDLE)_get_osfhandle(fderr[0]), HANDLE_FLAG_INHERIT, 0); /* First check, if there is a CGI interpreter configured for all CGI * scripts. */ interp = conn->dom_ctx->config[CGI_INTERPRETER + cgi_config_idx]; if (interp != NULL) { /* If there is a configured interpreter, check for additional arguments */ interp_arg = conn->dom_ctx->config[CGI_INTERPRETER_ARGS + cgi_config_idx]; } else { /* Otherwise, the interpreter must be stated in the first line of the * CGI script file, after a #! (shebang) mark. */ buf[0] = buf[1] = '\0'; /* Get the full script path */ mg_snprintf( conn, &truncated, cmdline, sizeof(cmdline), "%s/%s", dir, prog); if (truncated) { pi.hProcess = (pid_t)-1; goto spawn_cleanup; } /* Open the script file, to read the first line */ if (mg_fopen(conn, cmdline, MG_FOPEN_MODE_READ, &file)) { /* Read the first line of the script into the buffer */ mg_fgets(buf, sizeof(buf), &file); (void)mg_fclose(&file.access); /* ignore error on read only file */ buf[sizeof(buf) - 1] = '\0'; } if ((buf[0] == '#') && (buf[1] == '!')) { trim_trailing_whitespaces(buf + 2); } else { buf[2] = '\0'; } interp = buf + 2; } GetFullPathNameA(dir, sizeof(full_dir), full_dir, NULL); if (interp[0] != '\0') { /* This is an interpreted script file. We must call the interpreter. */ if ((interp_arg != 0) && (interp_arg[0] != 0)) { mg_snprintf(conn, &truncated, cmdline, sizeof(cmdline), "\"%s\" %s \"%s\\%s\"", interp, interp_arg, full_dir, prog); } else { mg_snprintf(conn, &truncated, cmdline, sizeof(cmdline), "\"%s\" \"%s\\%s\"", interp, full_dir, prog); } } else { /* This is (probably) a compiled program. We call it directly. */ mg_snprintf(conn, &truncated, cmdline, sizeof(cmdline), "\"%s\\%s\"", full_dir, prog); } if (truncated) { pi.hProcess = (pid_t)-1; goto spawn_cleanup; } DEBUG_TRACE("Running [%s]", cmdline); if (CreateProcessA(NULL, cmdline, NULL, NULL, TRUE, CREATE_NEW_PROCESS_GROUP, envblk, NULL, &si, &pi) == 0) { mg_cry_internal( conn, "%s: CreateProcess(%s): %ld", __func__, cmdline, (long)ERRNO); pi.hProcess = (pid_t)-1; /* goto spawn_cleanup; */ } spawn_cleanup: (void)CloseHandle(si.hStdOutput); (void)CloseHandle(si.hStdError); (void)CloseHandle(si.hStdInput); if (pi.hThread != NULL) { (void)CloseHandle(pi.hThread); } return (pid_t)pi.hProcess; } #endif /* !NO_CGI */ static int set_blocking_mode(SOCKET sock) { unsigned long non_blocking = 0; return ioctlsocket(sock, (long)FIONBIO, &non_blocking); } static int set_non_blocking_mode(SOCKET sock) { unsigned long non_blocking = 1; return ioctlsocket(sock, (long)FIONBIO, &non_blocking); } #else #if !defined(NO_FILESYSTEMS) static int mg_stat(const struct mg_connection *conn, const char *path, struct mg_file_stat *filep) { struct stat st; if (!filep) { return 0; } memset(filep, 0, sizeof(*filep)); if (mg_path_suspicious(conn, path)) { return 0; } if (0 == stat(path, &st)) { filep->size = (uint64_t)(st.st_size); filep->last_modified = st.st_mtime; filep->is_directory = S_ISDIR(st.st_mode); return 1; } return 0; } #endif /* NO_FILESYSTEMS */ static void set_close_on_exec(int fd, const struct mg_connection *conn /* may be null */, struct mg_context *ctx /* may be null */) { #if defined(__ZEPHYR__) (void)fd; (void)conn; (void)ctx; #else if (fcntl(fd, F_SETFD, FD_CLOEXEC) != 0) { if (conn || ctx) { struct mg_connection fc; mg_cry_internal((conn ? conn : fake_connection(&fc, ctx)), "%s: fcntl(F_SETFD FD_CLOEXEC) failed: %s", __func__, strerror(ERRNO)); } } #endif } CIVETWEB_API int mg_start_thread(mg_thread_func_t func, void *param) { pthread_t thread_id; pthread_attr_t attr; int result; (void)pthread_attr_init(&attr); (void)pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); #if defined(__ZEPHYR__) pthread_attr_setstack(&attr, &civetweb_main_stack, ZEPHYR_STACK_SIZE); #elif defined(USE_STACK_SIZE) && (USE_STACK_SIZE > 1) /* Compile-time option to control stack size, * e.g. -DUSE_STACK_SIZE=16384 */ (void)pthread_attr_setstacksize(&attr, USE_STACK_SIZE); #endif /* defined(USE_STACK_SIZE) && (USE_STACK_SIZE > 1) */ result = pthread_create(&thread_id, &attr, func, param); pthread_attr_destroy(&attr); return result; } /* Start a thread storing the thread context. */ static int mg_start_thread_with_id(mg_thread_func_t func, void *param, pthread_t *threadidptr) { pthread_t thread_id; pthread_attr_t attr; int result; (void)pthread_attr_init(&attr); #if defined(__ZEPHYR__) pthread_attr_setstack(&attr, &civetweb_worker_stacks[zephyr_worker_stack_index++], ZEPHYR_STACK_SIZE); #elif defined(USE_STACK_SIZE) && (USE_STACK_SIZE > 1) /* Compile-time option to control stack size, * e.g. -DUSE_STACK_SIZE=16384 */ (void)pthread_attr_setstacksize(&attr, USE_STACK_SIZE); #endif /* defined(USE_STACK_SIZE) && USE_STACK_SIZE > 1 */ result = pthread_create(&thread_id, &attr, func, param); pthread_attr_destroy(&attr); if ((result == 0) && (threadidptr != NULL)) { *threadidptr = thread_id; } return result; } /* Wait for a thread to finish. */ static int mg_join_thread(pthread_t threadid) { int result; result = pthread_join(threadid, NULL); return result; } #if !defined(NO_CGI) static pid_t spawn_process(struct mg_connection *conn, const char *prog, char *envblk, char *envp[], int fdin[2], int fdout[2], int fderr[2], const char *dir, int cgi_config_idx) { pid_t pid; const char *interp; (void)envblk; if ((pid = fork()) == -1) { /* Parent */ mg_cry_internal(conn, "%s: fork(): %s", __func__, strerror(ERRNO)); } else if (pid != 0) { /* Make sure children close parent-side descriptors. * The caller will close the child-side immediately. */ set_close_on_exec(fdin[1], conn, NULL); /* stdin write */ set_close_on_exec(fdout[0], conn, NULL); /* stdout read */ set_close_on_exec(fderr[0], conn, NULL); /* stderr read */ } else { /* Child */ if (chdir(dir) != 0) { mg_cry_internal( conn, "%s: chdir(%s): %s", __func__, dir, strerror(ERRNO)); } else if (dup2(fdin[0], 0) == -1) { mg_cry_internal(conn, "%s: dup2(%d, 0): %s", __func__, fdin[0], strerror(ERRNO)); } else if (dup2(fdout[1], 1) == -1) { mg_cry_internal(conn, "%s: dup2(%d, 1): %s", __func__, fdout[1], strerror(ERRNO)); } else if (dup2(fderr[1], 2) == -1) { mg_cry_internal(conn, "%s: dup2(%d, 2): %s", __func__, fderr[1], strerror(ERRNO)); } else { struct sigaction sa; /* Keep stderr and stdout in two different pipes. * Stdout will be sent back to the client, * stderr should go into a server error log. */ (void)close(fdin[0]); (void)close(fdout[1]); (void)close(fderr[1]); /* Close write end fdin and read end fdout and fderr */ (void)close(fdin[1]); (void)close(fdout[0]); (void)close(fderr[0]); /* After exec, all signal handlers are restored to their default * values, with one exception of SIGCHLD. According to * POSIX.1-2001 and Linux's implementation, SIGCHLD's handler * will leave unchanged after exec if it was set to be ignored. * Restore it to default action. */ memset(&sa, 0, sizeof(sa)); sa.sa_handler = SIG_DFL; sigaction(SIGCHLD, &sa, NULL); interp = conn->dom_ctx->config[CGI_INTERPRETER + cgi_config_idx]; if (interp == NULL) { /* no interpreter configured, call the program directly */ (void)execle(prog, prog, NULL, envp); mg_cry_internal(conn, "%s: execle(%s): %s", __func__, prog, strerror(ERRNO)); } else { /* call the configured interpreter */ const char *interp_args = conn->dom_ctx ->config[CGI_INTERPRETER_ARGS + cgi_config_idx]; if ((interp_args != NULL) && (interp_args[0] != 0)) { (void)execle(interp, interp, interp_args, prog, NULL, envp); } else { (void)execle(interp, interp, prog, NULL, envp); } mg_cry_internal(conn, "%s: execle(%s %s): %s", __func__, interp, prog, strerror(ERRNO)); } } exit(EXIT_FAILURE); } return pid; } #endif /* !NO_CGI */ static int set_non_blocking_mode(SOCKET sock) { int flags = fcntl(sock, F_GETFL, 0); if (flags < 0) { return -1; } if (fcntl(sock, F_SETFL, (flags | O_NONBLOCK)) < 0) { return -1; } return 0; } static int set_blocking_mode(SOCKET sock) { int flags = fcntl(sock, F_GETFL, 0); if (flags < 0) { return -1; } if (fcntl(sock, F_SETFL, flags & (~(int)(O_NONBLOCK))) < 0) { return -1; } return 0; } #endif /* _WIN32 / else */ /* End of initial operating system specific define block. */ /* Get a random number (independent of C rand function) */ static uint64_t get_random(void) { static uint64_t lfsr = 0; /* Linear feedback shift register */ static uint64_t lcg = 0; /* Linear congruential generator */ uint64_t now = mg_get_current_time_ns(); if (lfsr == 0) { /* lfsr will be only 0 if has not been initialized, * so this code is called only once. */ lfsr = mg_get_current_time_ns(); lcg = mg_get_current_time_ns(); } else { /* Get the next step of both random number generators. */ lfsr = (lfsr >> 1) | ((((lfsr >> 0) ^ (lfsr >> 1) ^ (lfsr >> 3) ^ (lfsr >> 4)) & 1) << 63); lcg = lcg * 6364136223846793005LL + 1442695040888963407LL; } /* Combining two pseudo-random number generators and a high resolution * part * of the current server time will make it hard (impossible?) to guess * the * next number. */ return (lfsr ^ lcg ^ now); } static int mg_poll(struct mg_pollfd *pfd, unsigned int n, int milliseconds, const stop_flag_t *stop_flag) { /* Call poll, but only for a maximum time of a few seconds. * This will allow to stop the server after some seconds, instead * of having to wait for a long socket timeout. */ int ms_now = SOCKET_TIMEOUT_QUANTUM; /* Sleep quantum in ms */ int check_pollerr = 0; if ((n == 1) && ((pfd[0].events & POLLERR) == 0)) { /* If we wait for only one file descriptor, wait on error as well */ pfd[0].events |= POLLERR; check_pollerr = 1; } do { int result; if (!STOP_FLAG_IS_ZERO(&*stop_flag)) { /* Shut down signal */ return -2; } if ((milliseconds >= 0) && (milliseconds < ms_now)) { ms_now = milliseconds; } result = poll(pfd, n, ms_now); if (result != 0) { int err = ERRNO; if ((result == 1) || (!ERROR_TRY_AGAIN(err))) { /* Poll returned either success (1) or error (-1). * Forward both to the caller. */ if ((check_pollerr) && ((pfd[0].revents & (POLLIN | POLLOUT | POLLERR)) == POLLERR)) { /* One and only file descriptor returned error */ return -1; } return result; } } /* Poll returned timeout (0). */ if (milliseconds > 0) { milliseconds -= ms_now; } } while (milliseconds > 0); /* timeout: return 0 */ return 0; } /* Write data to the IO channel - opened file descriptor, socket or SSL * descriptor. * Return value: * >=0 .. number of bytes successfully written * -1 .. timeout * -2 .. error */ static int push_inner(struct mg_context *ctx, FILE *fp, SOCKET sock, SSL *ssl, const char *buf, int len, double timeout) { uint64_t start = 0, now = 0, timeout_ns = 0; int n, err; unsigned ms_wait = SOCKET_TIMEOUT_QUANTUM; /* Sleep quantum in ms */ #if defined(_WIN32) typedef int len_t; #else typedef size_t len_t; #endif if (timeout > 0) { now = mg_get_current_time_ns(); start = now; timeout_ns = (uint64_t)(timeout * 1.0E9); } if (ctx == NULL) { return -2; } #if defined(NO_SSL) && !defined(USE_MBEDTLS) if (ssl) { return -2; } #endif /* Try to read until it succeeds, fails, times out, or the server * shuts down. */ for (;;) { #if defined(USE_MBEDTLS) if (ssl != NULL) { n = mbed_ssl_write(ssl, (const unsigned char *)buf, len); if (n <= 0) { if ((n == MBEDTLS_ERR_SSL_WANT_READ) || (n == MBEDTLS_ERR_SSL_WANT_WRITE) || n == MBEDTLS_ERR_SSL_ASYNC_IN_PROGRESS) { n = 0; } else { fprintf(stderr, "SSL write failed, error %d\n", n); return -2; } } else { err = 0; } } else #elif !defined(NO_SSL) if (ssl != NULL) { ERR_clear_error(); n = SSL_write(ssl, buf, len); if (n <= 0) { err = SSL_get_error(ssl, n); if ((err == SSL_ERROR_SYSCALL) && (n == -1)) { err = ERRNO; } else if ((err == SSL_ERROR_WANT_READ) || (err == SSL_ERROR_WANT_WRITE)) { n = 0; } else { DEBUG_TRACE("SSL_write() failed, error %d", err); ERR_clear_error(); return -2; } ERR_clear_error(); } else { err = 0; } } else #endif if (fp != NULL) { n = (int)fwrite(buf, 1, (size_t)len, fp); if (ferror(fp)) { n = -1; err = ERRNO; } else { err = 0; } } else { n = (int)send(sock, buf, (len_t)len, MSG_NOSIGNAL); err = (n < 0) ? ERRNO : 0; if (ERROR_TRY_AGAIN(err)) { err = 0; n = 0; } if (n < 0) { /* shutdown of the socket at client side */ return -2; } } if (!STOP_FLAG_IS_ZERO(&ctx->stop_flag)) { return -2; } if ((n > 0) || ((n == 0) && (len == 0))) { /* some data has been read, or no data was requested */ return n; } if (n < 0) { /* socket error - check errno */ DEBUG_TRACE("send() failed, error %d", err); /* TODO (mid): error handling depending on the error code. * These codes are different between Windows and Linux. * Currently there is no problem with failing send calls, * if there is a reproducible situation, it should be * investigated in detail. */ return -2; } /* Only in case n=0 (timeout), repeat calling the write function */ /* If send failed, wait before retry */ if (fp != NULL) { /* For files, just wait a fixed time. * Maybe it helps, maybe not. */ mg_sleep(5); } else { /* For sockets, wait for the socket using poll */ struct mg_pollfd pfd[1]; int pollres; pfd[0].fd = sock; pfd[0].events = POLLOUT; pollres = mg_poll(pfd, 1, (int)(ms_wait), &(ctx->stop_flag)); if (!STOP_FLAG_IS_ZERO(&ctx->stop_flag)) { return -2; } if (pollres > 0) { continue; } } if (timeout > 0) { now = mg_get_current_time_ns(); if ((now - start) > timeout_ns) { /* Timeout */ break; } } } (void)err; /* Avoid unused warning if NO_SSL is set and DEBUG_TRACE is not used */ return -1; } static int push_all(struct mg_context *ctx, FILE *fp, SOCKET sock, SSL *ssl, const char *buf, int len) { double timeout = -1.0; int n, nwritten = 0; if (ctx == NULL) { return -1; } if (ctx->dd.config[REQUEST_TIMEOUT]) { timeout = atoi(ctx->dd.config[REQUEST_TIMEOUT]) / 1000.0; } if (timeout <= 0.0) { timeout = strtod(config_options[REQUEST_TIMEOUT].default_value, NULL) / 1000.0; } while ((len > 0) && STOP_FLAG_IS_ZERO(&ctx->stop_flag)) { n = push_inner(ctx, fp, sock, ssl, buf + nwritten, len, timeout); if (n < 0) { if (nwritten == 0) { nwritten = -1; /* Propagate the error */ } break; } else if (n == 0) { break; /* No more data to write */ } else { nwritten += n; len -= n; } } return nwritten; } /* Read from IO channel - opened file descriptor, socket, or SSL descriptor. * Return value: * >=0 .. number of bytes successfully read * -1 .. timeout * -2 .. error */ static int pull_inner(FILE *fp, struct mg_connection *conn, char *buf, int len, double timeout) { int nread, err = 0; #if defined(_WIN32) typedef int len_t; #else typedef size_t len_t; #endif /* We need an additional wait loop around this, because in some cases * with TLSwe may get data from the socket but not from SSL_read. * In this case we need to repeat at least once. */ if (fp != NULL) { /* Use read() instead of fread(), because if we're reading from the * CGI pipe, fread() may block until IO buffer is filled up. We * cannot afford to block and must pass all read bytes immediately * to the client. */ nread = (int)read(fileno(fp), buf, (size_t)len); err = (nread < 0) ? ERRNO : 0; if ((nread == 0) && (len > 0)) { /* Should get data, but got EOL */ return -2; } #if defined(USE_MBEDTLS) } else if (conn->ssl != NULL) { struct mg_pollfd pfd[1]; int to_read; int pollres; to_read = mbedtls_ssl_get_bytes_avail(conn->ssl); if (to_read > 0) { /* We already know there is no more data buffered in conn->buf * but there is more available in the SSL layer. So don't poll * conn->client.sock yet. */ pollres = 1; if (to_read > len) to_read = len; } else { pfd[0].fd = conn->client.sock; pfd[0].events = POLLIN; to_read = len; pollres = mg_poll(pfd, 1, (int)(timeout * 1000.0), &(conn->phys_ctx->stop_flag)); if (!STOP_FLAG_IS_ZERO(&conn->phys_ctx->stop_flag)) { return -2; } } if (pollres > 0) { nread = mbed_ssl_read(conn->ssl, (unsigned char *)buf, to_read); if (nread <= 0) { if ((nread == MBEDTLS_ERR_SSL_WANT_READ) || (nread == MBEDTLS_ERR_SSL_WANT_WRITE) || nread == MBEDTLS_ERR_SSL_ASYNC_IN_PROGRESS) { nread = 0; } else { fprintf(stderr, "SSL read failed, error %d\n", nread); return -2; } } else { err = 0; } } else if (pollres < 0) { /* Error */ return -2; } else { /* pollres = 0 means timeout */ nread = 0; } #elif !defined(NO_SSL) } else if (conn->ssl != NULL) { int ssl_pending; struct mg_pollfd pfd[1]; int pollres; if ((ssl_pending = SSL_pending(conn->ssl)) > 0) { /* We already know there is no more data buffered in conn->buf * but there is more available in the SSL layer. So don't poll * conn->client.sock yet. */ if (ssl_pending > len) { ssl_pending = len; } pollres = 1; } else { pfd[0].fd = conn->client.sock; pfd[0].events = POLLIN; pollres = mg_poll(pfd, 1, (int)(timeout * 1000.0), &(conn->phys_ctx->stop_flag)); if (!STOP_FLAG_IS_ZERO(&conn->phys_ctx->stop_flag)) { return -2; } } if (pollres > 0) { ERR_clear_error(); nread = SSL_read(conn->ssl, buf, (ssl_pending > 0) ? ssl_pending : len); if (nread <= 0) { err = SSL_get_error(conn->ssl, nread); if ((err == SSL_ERROR_SYSCALL) && (nread == -1)) { err = ERRNO; } else if ((err == SSL_ERROR_WANT_READ) || (err == SSL_ERROR_WANT_WRITE)) { nread = 0; } else { /* All errors should return -2 */ DEBUG_TRACE("SSL_read() failed, error %d", err); ERR_clear_error(); return -2; } ERR_clear_error(); } else { err = 0; } } else if (pollres < 0) { /* Error */ return -2; } else { /* pollres = 0 means timeout */ nread = 0; } #endif } else { struct mg_pollfd pfd[1]; int pollres; pfd[0].fd = conn->client.sock; pfd[0].events = POLLIN; pollres = mg_poll(pfd, 1, (int)(timeout * 1000.0), &(conn->phys_ctx->stop_flag)); if (!STOP_FLAG_IS_ZERO(&conn->phys_ctx->stop_flag)) { return -2; } if (pollres > 0) { nread = (int)recv(conn->client.sock, buf, (len_t)len, 0); err = (nread < 0) ? ERRNO : 0; if (nread <= 0) { /* shutdown of the socket at client side */ return -2; } } else if (pollres < 0) { /* error calling poll */ return -2; } else { /* pollres = 0 means timeout */ nread = 0; } } if (!STOP_FLAG_IS_ZERO(&conn->phys_ctx->stop_flag)) { return -2; } if ((nread > 0) || ((nread == 0) && (len == 0))) { /* some data has been read, or no data was requested */ return nread; } if (nread < 0) { /* socket error - check errno */ #if defined(_WIN32) if (err == WSAEWOULDBLOCK) { /* TODO (low): check if this is still required */ /* standard case if called from close_socket_gracefully */ return -2; } else if (err == WSAETIMEDOUT) { /* TODO (low): check if this is still required */ /* timeout is handled by the while loop */ return 0; } else if (err == WSAECONNABORTED) { /* See https://www.chilkatsoft.com/p/p_299.asp */ return -2; } else { DEBUG_TRACE("recv() failed, error %d", err); return -2; } #else /* TODO: POSIX returns either EAGAIN or EWOULDBLOCK in both cases, * if the timeout is reached and if the socket was set to non- * blocking in close_socket_gracefully, so we can not distinguish * here. We have to wait for the timeout in both cases for now. */ if (ERROR_TRY_AGAIN(err)) { /* TODO (low): check if this is still required */ /* EAGAIN/EWOULDBLOCK: * standard case if called from close_socket_gracefully * => should return -1 */ /* or timeout occurred * => the code must stay in the while loop */ /* EINTR can be generated on a socket with a timeout set even * when SA_RESTART is effective for all relevant signals * (see signal(7)). * => stay in the while loop */ } else { DEBUG_TRACE("recv() failed, error %d", err); return -2; } #endif } /* Timeout occurred, but no data available. */ return -1; } static int pull_all(FILE *fp, struct mg_connection *conn, char *buf, int len) { int n, nread = 0; double timeout = -1.0; uint64_t start_time = 0, now = 0, timeout_ns = 0; if (conn->dom_ctx->config[REQUEST_TIMEOUT]) { timeout = atoi(conn->dom_ctx->config[REQUEST_TIMEOUT]) / 1000.0; } if (timeout <= 0.0) { timeout = strtod(config_options[REQUEST_TIMEOUT].default_value, NULL) / 1000.0; } start_time = mg_get_current_time_ns(); timeout_ns = (uint64_t)(timeout * 1.0E9); while ((len > 0) && STOP_FLAG_IS_ZERO(&conn->phys_ctx->stop_flag)) { n = pull_inner(fp, conn, buf + nread, len, timeout); if (n == -2) { if (nread == 0) { nread = -1; /* Propagate the error */ } break; } else if (n == -1) { /* timeout */ if (timeout >= 0.0) { now = mg_get_current_time_ns(); if ((now - start_time) <= timeout_ns) { continue; } } break; } else if (n == 0) { break; /* No more data to read */ } else { nread += n; len -= n; } } return nread; } static void discard_unread_request_data(struct mg_connection *conn) { char buf[MG_BUF_LEN]; while (mg_read(conn, buf, sizeof(buf)) > 0) ; } static int mg_read_inner(struct mg_connection *conn, void *buf, size_t len) { int64_t content_len, n, buffered_len, nread; int64_t len64 = (int64_t)((len > INT_MAX) ? INT_MAX : len); /* since the return value is * int, we may not read more * bytes */ const char *body; if (conn == NULL) { return 0; } /* If Content-Length is not set for a response with body data, * we do not know in advance how much data should be read. */ content_len = conn->content_len; if (content_len < 0) { /* The body data is completed when the connection is closed. */ content_len = INT64_MAX; } nread = 0; if (conn->consumed_content < content_len) { /* Adjust number of bytes to read. */ int64_t left_to_read = content_len - conn->consumed_content; if (left_to_read < len64) { /* Do not read more than the total content length of the * request. */ len64 = left_to_read; } /* Return buffered data */ buffered_len = (int64_t)(conn->data_len) - (int64_t)conn->request_len - conn->consumed_content; if (buffered_len > 0) { if (len64 < buffered_len) { buffered_len = len64; } body = conn->buf + conn->request_len + conn->consumed_content; memcpy(buf, body, (size_t)buffered_len); len64 -= buffered_len; conn->consumed_content += buffered_len; nread += buffered_len; buf = (char *)buf + buffered_len; } /* We have returned all buffered data. Read new data from the remote * socket. */ if ((n = pull_all(NULL, conn, (char *)buf, (int)len64)) >= 0) { conn->consumed_content += n; nread += n; } else { nread = ((nread > 0) ? nread : n); } } return (int)nread; } /* Forward declarations */ static void handle_request(struct mg_connection *); static void log_access(const struct mg_connection *); /* Handle request, update statistics and call access log */ static void handle_request_stat_log(struct mg_connection *conn) { #if defined(USE_SERVER_STATS) struct timespec tnow; conn->conn_state = 4; /* processing */ #endif handle_request(conn); #if defined(USE_SERVER_STATS) conn->conn_state = 5; /* processed */ clock_gettime(CLOCK_MONOTONIC, &tnow); conn->processing_time = mg_difftimespec(&tnow, &(conn->req_time)); mg_atomic_add64(&(conn->phys_ctx->total_data_read), conn->consumed_content); mg_atomic_add64(&(conn->phys_ctx->total_data_written), conn->num_bytes_sent); #endif DEBUG_TRACE("%s", "handle_request done"); if (conn->phys_ctx->callbacks.end_request != NULL) { conn->phys_ctx->callbacks.end_request(conn, conn->status_code); DEBUG_TRACE("%s", "end_request callback done"); } log_access(conn); } #if defined(USE_HTTP2) #if defined(NO_SSL) #error "HTTP2 requires ALPN, ALPN requires SSL/TLS" #endif #define USE_ALPN #include "http2.h" /* Not supported with HTTP/2 */ #define HTTP1_only \ { \ if (conn->protocol_type == PROTOCOL_TYPE_HTTP2) { \ http2_must_use_http1(conn); \ DEBUG_TRACE("%s", "must use HTTP/1.x"); \ return; \ } \ } #else #define HTTP1_only #endif CIVETWEB_API int mg_read(struct mg_connection *conn, void *buf, size_t len) { if (len > INT_MAX) { len = INT_MAX; } if (conn == NULL) { return 0; } if (conn->is_chunked) { size_t all_read = 0; while (len > 0) { if (conn->is_chunked >= 3) { /* No more data left to read */ return 0; } if (conn->is_chunked != 1) { /* Has error */ return -1; } if (conn->consumed_content != conn->content_len) { /* copy from the current chunk */ int read_ret = mg_read_inner(conn, (char *)buf + all_read, len); if (read_ret < 1) { /* read error */ conn->is_chunked = 2; return -1; } all_read += (size_t)read_ret; len -= (size_t)read_ret; if (conn->consumed_content == conn->content_len) { /* Add data bytes in the current chunk have been read, * so we are expecting \r\n now. */ char x[2]; conn->content_len += 2; if ((mg_read_inner(conn, x, 2) != 2) || (x[0] != '\r') || (x[1] != '\n')) { /* Protocol violation */ conn->is_chunked = 2; return -1; } } } else { /* fetch a new chunk */ size_t i; char lenbuf[64]; char *end = NULL; unsigned long chunkSize = 0; for (i = 0; i < (sizeof(lenbuf) - 1); i++) { conn->content_len++; if (mg_read_inner(conn, lenbuf + i, 1) != 1) { lenbuf[i] = 0; } if ((i > 0) && (lenbuf[i] == ';')) { // chunk extension --> skip chars until next CR // // RFC 2616, 3.6.1 Chunked Transfer Coding // (https://www.rfc-editor.org/rfc/rfc2616#page-25) // // chunk = chunk-size [ chunk-extension ] CRLF // chunk-data CRLF // ... // chunk-extension= *( ";" chunk-ext-name [ "=" // chunk-ext-val ] ) do ++conn->content_len; while (mg_read_inner(conn, lenbuf + i, 1) == 1 && lenbuf[i] != '\r'); } if ((i > 0) && (lenbuf[i] == '\r') && (lenbuf[i - 1] != '\r')) { continue; } if ((i > 1) && (lenbuf[i] == '\n') && (lenbuf[i - 1] == '\r')) { lenbuf[i + 1] = 0; chunkSize = strtoul(lenbuf, &end, 16); if (chunkSize == 0) { /* regular end of content */ conn->is_chunked = 3; } break; } if (!isxdigit((unsigned char)lenbuf[i])) { /* illegal character for chunk length */ conn->is_chunked = 2; return -1; } } if ((end == NULL) || (*end != '\r')) { /* chunksize not set correctly */ conn->is_chunked = 2; return -1; } if (conn->is_chunked == 3) { /* try discarding trailer for keep-alive */ // We found the last chunk (length 0) including the // CRLF that terminates that chunk. Now follows a possibly // empty trailer and a final CRLF. // // see RFC 2616, 3.6.1 Chunked Transfer Coding // (https://www.rfc-editor.org/rfc/rfc2616#page-25) // // Chunked-Body = *chunk // last-chunk // trailer // CRLF // ... // last-chunk = 1*("0") [ chunk-extension ] CRLF // ... // trailer = *(entity-header CRLF) int crlf_count = 2; // one CRLF already determined while (crlf_count < 4 && conn->is_chunked == 3) { ++conn->content_len; if (mg_read_inner(conn, lenbuf, 1) == 1) { if ((crlf_count == 0 || crlf_count == 2)) { if (lenbuf[0] == '\r') ++crlf_count; else crlf_count = 0; } else { // previous character was a CR // --> next character must be LF if (lenbuf[0] == '\n') ++crlf_count; else conn->is_chunked = 2; } } else // premature end of trailer conn->is_chunked = 2; } if (conn->is_chunked == 2) return -1; else conn->is_chunked = 4; break; } /* append a new chunk */ conn->content_len += (int64_t)chunkSize; } } return (int)all_read; } return mg_read_inner(conn, buf, len); } CIVETWEB_API int mg_write(struct mg_connection *conn, const void *buf, size_t len) { time_t now; int n, total, allowed; if (conn == NULL) { return 0; } if (len > INT_MAX) { return -1; } /* Mark connection as "data sent" */ conn->request_state = 10; #if defined(USE_HTTP2) if (conn->protocol_type == PROTOCOL_TYPE_HTTP2) { http2_data_frame_head(conn, len, 0); } #endif if (conn->throttle > 0) { if ((now = time(NULL)) != conn->last_throttle_time) { conn->last_throttle_time = now; conn->last_throttle_bytes = 0; } allowed = conn->throttle - conn->last_throttle_bytes; if (allowed > (int)len) { allowed = (int)len; } total = push_all(conn->phys_ctx, NULL, conn->client.sock, conn->ssl, (const char *)buf, allowed); if (total == allowed) { buf = (const char *)buf + total; conn->last_throttle_bytes += total; while ((total < (int)len) && STOP_FLAG_IS_ZERO(&conn->phys_ctx->stop_flag)) { allowed = (conn->throttle > ((int)len - total)) ? (int)len - total : conn->throttle; n = push_all(conn->phys_ctx, NULL, conn->client.sock, conn->ssl, (const char *)buf, allowed); if (n != allowed) { break; } sleep(1); conn->last_throttle_bytes = allowed; conn->last_throttle_time = time(NULL); buf = (const char *)buf + n; total += n; } } } else { total = push_all(conn->phys_ctx, NULL, conn->client.sock, conn->ssl, (const char *)buf, (int)len); } if (total > 0) { conn->num_bytes_sent += total; } return total; } /* Send a chunk, if "Transfer-Encoding: chunked" is used */ CIVETWEB_API int mg_send_chunk(struct mg_connection *conn, const char *chunk, unsigned int chunk_len) { char lenbuf[16]; size_t lenbuf_len; int ret; int t; /* First store the length information in a text buffer. */ snprintf(lenbuf, sizeof(lenbuf), "%x\r\n", chunk_len); lenbuf_len = strlen(lenbuf); /* Then send length information, chunk and terminating \r\n. */ ret = mg_write(conn, lenbuf, lenbuf_len); if (ret != (int)lenbuf_len) { return -1; } t = ret; ret = mg_write(conn, chunk, chunk_len); if (ret != (int)chunk_len) { return -1; } t += ret; ret = mg_write(conn, "\r\n", 2); if (ret != 2) { return -1; } t += ret; return t; } #if defined(GCC_DIAGNOSTIC) /* This block forwards format strings to printf implementations, * so we need to disable the format-nonliteral warning. */ # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wformat-nonliteral" #endif /* Alternative alloc_vprintf() for non-compliant C runtimes */ static int alloc_vprintf2(char **buf, const char *fmt, va_list ap) { va_list ap_copy; size_t size = MG_BUF_LEN / 4; int len = -1; *buf = NULL; while (len < 0) { if (*buf) { mg_free(*buf); } size *= 4; *buf = (char *)mg_malloc(size); if (!*buf) { break; } va_copy(ap_copy, ap); len = vsnprintf_impl(*buf, size - 1, fmt, ap_copy); va_end(ap_copy); (*buf)[size - 1] = 0; } return len; } /* Print message to buffer. If buffer is large enough to hold the message, * return buffer. If buffer is to small, allocate large enough buffer on * heap, * and return allocated buffer. */ static int alloc_vprintf(char **out_buf, char *prealloc_buf, size_t prealloc_size, const char *fmt, va_list ap) { va_list ap_copy; int len; /* Windows is not standard-compliant, and vsnprintf() returns -1 if * buffer is too small. Also, older versions of msvcrt.dll do not have * _vscprintf(). However, if size is 0, vsnprintf() behaves correctly. * Therefore, we make two passes: on first pass, get required message * length. * On second pass, actually print the message. */ va_copy(ap_copy, ap); len = vsnprintf_impl(NULL, 0, fmt, ap_copy); va_end(ap_copy); if (len < 0) { /* C runtime is not standard compliant, vsnprintf() returned -1. * Switch to alternative code path that uses incremental * allocations. */ va_copy(ap_copy, ap); len = alloc_vprintf2(out_buf, fmt, ap_copy); va_end(ap_copy); } else if ((size_t)(len) >= prealloc_size) { /* The pre-allocated buffer not large enough. */ /* Allocate a new buffer. */ *out_buf = (char *)mg_malloc((size_t)(len) + 1); if (!*out_buf) { /* Allocation failed. Return -1 as "out of memory" error. */ return -1; } /* Buffer allocation successful. Store the string there. */ va_copy(ap_copy, ap); IGNORE_UNUSED_RESULT( vsnprintf_impl(*out_buf, (size_t)(len) + 1, fmt, ap_copy)); va_end(ap_copy); } else { /* The pre-allocated buffer is large enough. * Use it to store the string and return the address. */ va_copy(ap_copy, ap); IGNORE_UNUSED_RESULT( vsnprintf_impl(prealloc_buf, prealloc_size, fmt, ap_copy)); va_end(ap_copy); *out_buf = prealloc_buf; } return len; } static int alloc_printf(char **out_buf, const char *fmt, ...) { va_list ap; int result; va_start(ap, fmt); result = alloc_vprintf(out_buf, NULL, 0, fmt, ap); va_end(ap); return result; } #if defined(GCC_DIAGNOSTIC) /* Enable format-nonliteral warning again. */ # pragma GCC diagnostic pop #endif static int mg_vprintf(struct mg_connection *conn, const char *fmt, va_list ap) { char mem[MG_BUF_LEN]; char *buf = NULL; int len; if ((len = alloc_vprintf(&buf, mem, sizeof(mem), fmt, ap)) > 0) { len = mg_write(conn, buf, (size_t)len); } if (buf != mem) { mg_free(buf); } return len; } CIVETWEB_API int mg_printf(struct mg_connection *conn, const char *fmt, ...) { va_list ap; int result; va_start(ap, fmt); result = mg_vprintf(conn, fmt, ap); va_end(ap); return result; } CIVETWEB_API int mg_url_decode(const char *src, int src_len, char *dst, int dst_len, int is_form_url_encoded) { int i, j, a, b; #define HEXTOI(x) (isdigit(x) ? (x - '0') : (x - 'W')) for (i = j = 0; (i < src_len) && (j < (dst_len - 1)); i++, j++) { if ((i < src_len - 2) && (src[i] == '%') && isxdigit((unsigned char)src[i + 1]) && isxdigit((unsigned char)src[i + 2])) { a = tolower((unsigned char)src[i + 1]); b = tolower((unsigned char)src[i + 2]); dst[j] = (char)((HEXTOI(a) << 4) | HEXTOI(b)); i += 2; } else if (is_form_url_encoded && (src[i] == '+')) { dst[j] = ' '; } else { dst[j] = src[i]; } } dst[j] = '\0'; /* Null-terminate the destination */ return (i >= src_len) ? j : -1; } /* form url decoding of an entire string */ static void url_decode_in_place(char *buf) { int len = (int)strlen(buf); (void)mg_url_decode(buf, len, buf, len + 1, 1); } CIVETWEB_API int mg_get_var(const char *data, size_t data_len, const char *name, char *dst, size_t dst_len) { return mg_get_var2(data, data_len, name, dst, dst_len, 0); } CIVETWEB_API int mg_get_var2(const char *data, size_t data_len, const char *name, char *dst, size_t dst_len, size_t occurrence) { const char *p, *e, *s; size_t name_len; int len; if ((dst == NULL) || (dst_len == 0)) { len = -2; } else if ((data == NULL) || (name == NULL) || (data_len == 0)) { len = -1; dst[0] = '\0'; } else { name_len = strlen(name); e = data + data_len; len = -1; dst[0] = '\0'; /* data is "var1=val1&var2=val2...". Find variable first */ for (p = data; p + name_len < e; p++) { if (((p == data) || (p[-1] == '&')) && (p[name_len] == '=') && !mg_strncasecmp(name, p, name_len) && 0 == occurrence--) { /* Point p to variable value */ p += name_len + 1; /* Point s to the end of the value */ s = (const char *)memchr(p, '&', (size_t)(e - p)); if (s == NULL) { s = e; } DEBUG_ASSERT(s >= p); if (s < p) { return -3; } /* Decode variable into destination buffer */ len = mg_url_decode(p, (int)(s - p), dst, (int)dst_len, 1); /* Redirect error code from -1 to -2 (destination buffer too * small). */ if (len == -1) { len = -2; } break; } } } return len; } /* split a string "key1=val1&key2=val2" into key/value pairs */ CIVETWEB_API int mg_split_form_urlencoded(char *data, struct mg_header *form_fields, unsigned num_form_fields) { char *b; int i; int num = 0; if (data == NULL) { /* parameter error */ return -1; } if ((form_fields == NULL) && (num_form_fields == 0)) { /* determine the number of expected fields */ if (data[0] == 0) { return 0; } /* count number of & to return the number of key-value-pairs */ num = 1; while (*data) { if (*data == '&') { num++; } data++; } return num; } if ((form_fields == NULL) || ((int)num_form_fields <= 0)) { /* parameter error */ return -1; } for (i = 0; i < (int)num_form_fields; i++) { /* extract key-value pairs from input data */ while ((*data == ' ') || (*data == '\t')) { /* skip initial spaces */ data++; } if (*data == 0) { /* end of string reached */ break; } form_fields[num].name = data; /* find & or = */ b = data; while ((*b != 0) && (*b != '&') && (*b != '=')) { b++; } if (*b == 0) { /* last key without value */ form_fields[num].value = NULL; } else if (*b == '&') { /* mid key without value */ form_fields[num].value = NULL; } else { /* terminate string */ *b = 0; /* value starts after '=' */ data = b + 1; form_fields[num].value = data; } /* new field is stored */ num++; /* find a next key */ b = strchr(data, '&'); if (b == 0) { /* no more data */ break; } else { /* terminate value of last field at '&' */ *b = 0; /* next key-value-pairs starts after '&' */ data = b + 1; } } /* Decode all values */ for (i = 0; i < num; i++) { if (form_fields[i].name) { url_decode_in_place((char *)form_fields[i].name); } if (form_fields[i].value) { url_decode_in_place((char *)form_fields[i].value); } } /* return number of fields found */ return num; } /* HCP24: some changes to compare whole var_name */ CIVETWEB_API int mg_get_cookie(const char *cookie_header, const char *var_name, char *dst, size_t dst_size) { const char *s, *p, *end; int name_len, len = -1; if ((dst == NULL) || (dst_size == 0)) { return -2; } dst[0] = '\0'; if ((var_name == NULL) || ((s = cookie_header) == NULL)) { return -1; } name_len = (int)strlen(var_name); end = s + strlen(s); for (; (s = mg_strcasestr(s, var_name)) != NULL; s += name_len) { if (s[name_len] == '=') { /* HCP24: now check is it a substring or a full cookie name */ if ((s == cookie_header) || (s[-1] == ' ')) { s += name_len + 1; if ((p = strchr(s, ' ')) == NULL) { p = end; } if (p[-1] == ';') { p--; } if ((*s == '"') && (p[-1] == '"') && (p > s + 1)) { s++; p--; } if ((size_t)(p - s) < dst_size) { len = (int)(p - s); mg_strlcpy(dst, s, (size_t)len + 1); } else { len = -3; } break; } } } return len; } CIVETWEB_API int mg_base64_encode(const unsigned char *src, size_t src_len, char *dst, size_t *dst_len) { static const char *b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; size_t i, j; int a, b, c; if (dst_len != NULL) { /* Expected length including 0 termination: */ /* IN 1 -> OUT 5, IN 2 -> OUT 5, IN 3 -> OUT 5, IN 4 -> OUT 9, * IN 5 -> OUT 9, IN 6 -> OUT 9, IN 7 -> OUT 13, etc. */ size_t expected_len = ((src_len + 2) / 3) * 4 + 1; if (*dst_len < expected_len) { if (*dst_len > 0) { dst[0] = '\0'; } *dst_len = expected_len; return 0; } } for (i = j = 0; i < src_len; i += 3) { a = src[i]; b = ((i + 1) >= src_len) ? 0 : src[i + 1]; c = ((i + 2) >= src_len) ? 0 : src[i + 2]; dst[j++] = b64[a >> 2]; dst[j++] = b64[((a & 3) << 4) | (b >> 4)]; if (i + 1 < src_len) { dst[j++] = b64[(b & 15) << 2 | (c >> 6)]; } if (i + 2 < src_len) { dst[j++] = b64[c & 63]; } } while (j % 4 != 0) { dst[j++] = '='; } dst[j++] = '\0'; if (dst_len != NULL) { *dst_len = (size_t)j; } /* Return -1 for "OK" */ return -1; } static unsigned char b64reverse(char letter) { if ((letter >= 'A') && (letter <= 'Z')) { return (unsigned char)(letter - 'A'); } if ((letter >= 'a') && (letter <= 'z')) { return (unsigned char)(letter - 'a' + 26); } if ((letter >= '0') && (letter <= '9')) { return (unsigned char)(letter - '0' + 52); } if (letter == '+') { return 62; } if (letter == '/') { return 63; } if (letter == '=') { return 255; /* normal end */ } return 254; /* error */ } CIVETWEB_API int mg_base64_decode(const char *src, size_t src_len, unsigned char *dst, size_t *dst_len) { size_t i; unsigned char a, b, c, d; size_t dst_len_limit = (size_t)-1; size_t dst_len_used = 0; if (dst_len != NULL) { dst_len_limit = *dst_len; *dst_len = 0; } for (i = 0; i < src_len; i += 4) { /* Read 4 characters from BASE64 string */ a = b64reverse(src[i]); if (a >= 254) { return (int)i; } b = b64reverse(((i + 1) >= src_len) ? 0 : src[i + 1]); if (b >= 254) { return (int)i + 1; } c = b64reverse(((i + 2) >= src_len) ? 0 : src[i + 2]); if (c == 254) { return (int)i + 2; } d = b64reverse(((i + 3) >= src_len) ? 0 : src[i + 3]); if (d == 254) { return (int)i + 3; } /* Add first (of 3) decoded character */ if (dst_len_used < dst_len_limit) { dst[dst_len_used] = (unsigned char)((unsigned char)(a << 2) + (unsigned char)(b >> 4)); } dst_len_used++; if (c != 255) { if (dst_len_used < dst_len_limit) { dst[dst_len_used] = (unsigned char)((unsigned char)(b << 4) + (unsigned char)(c >> 2)); } dst_len_used++; if (d != 255) { if (dst_len_used < dst_len_limit) { dst[dst_len_used] = (unsigned char)((unsigned char)(c << 6) + d); } dst_len_used++; } } } /* Add terminating zero */ if (dst_len_used < dst_len_limit) { dst[dst_len_used] = '\0'; } dst_len_used++; if (dst_len != NULL) { *dst_len = dst_len_used; } if (dst_len_used > dst_len_limit) { /* Out of memory */ return 0; } /* Return -1 for "OK" */ return -1; } static int is_put_or_delete_method(const struct mg_connection *conn) { if (conn) { const char *s = conn->request_info.request_method; if (s != NULL) { /* PUT, DELETE, MKCOL, PATCH, LOCK, UNLOCK, PROPPATCH, MOVE, COPY */ return (!strcmp(s, "PUT") || !strcmp(s, "DELETE") || !strcmp(s, "MKCOL") || !strcmp(s, "PATCH") || !strcmp(s, "LOCK") || !strcmp(s, "UNLOCK") || !strcmp(s, "PROPPATCH") || !strcmp(s, "MOVE") || !strcmp(s, "COPY")); } } return 0; } static int is_civetweb_webdav_method(const struct mg_connection *conn) { /* Note: Here we only have to identify the WebDav methods that need special * handling in the CivetWeb code - not all methods used in WebDav. In * particular, methods used on directories (when using Windows Explorer as * WebDav client). */ if (conn) { const char *s = conn->request_info.request_method; if (s != NULL) { /* These are the civetweb builtin DAV methods */ return (!strcmp(s, "PROPFIND") || !strcmp(s, "PROPPATCH") || !strcmp(s, "LOCK") || !strcmp(s, "UNLOCK") || !strcmp(s, "MOVE") || !strcmp(s, "COPY")); } } return 0; } #if !defined(NO_FILES) static int extention_matches_script( struct mg_connection *conn, /* in: request (must be valid) */ const char *filename /* in: filename (must be valid) */ ) { #if !defined(NO_CGI) int cgi_config_idx, inc, max; #endif #if defined(USE_LUA) if (match_prefix_strlen(conn->dom_ctx->config[LUA_SCRIPT_EXTENSIONS], filename) > 0) { return 1; } #endif #if defined(USE_DUKTAPE) if (match_prefix_strlen(conn->dom_ctx->config[DUKTAPE_SCRIPT_EXTENSIONS], filename) > 0) { return 1; } #endif #if !defined(NO_CGI) inc = CGI2_EXTENSIONS - CGI_EXTENSIONS; max = PUT_DELETE_PASSWORDS_FILE - CGI_EXTENSIONS; for (cgi_config_idx = 0; cgi_config_idx < max; cgi_config_idx += inc) { if ((conn->dom_ctx->config[CGI_EXTENSIONS + cgi_config_idx] != NULL) && (match_prefix_strlen( conn->dom_ctx->config[CGI_EXTENSIONS + cgi_config_idx], filename) > 0)) { return 1; } } #endif /* filename and conn could be unused, if all preocessor conditions * are false (no script language supported). */ (void)filename; (void)conn; return 0; } static int extention_matches_template_text( struct mg_connection *conn, /* in: request (must be valid) */ const char *filename /* in: filename (must be valid) */ ) { #if defined(USE_LUA) if (match_prefix_strlen(conn->dom_ctx->config[LUA_SERVER_PAGE_EXTENSIONS], filename) > 0) { return 1; } #endif if (match_prefix_strlen(conn->dom_ctx->config[SSI_EXTENSIONS], filename) > 0) { return 1; } return 0; } /* For given directory path, substitute it to valid index file. * Return 1 if index file has been found, 0 if not found. * If the file is found, it's stats is returned in stp. */ static int substitute_index_file(struct mg_connection *conn, char *path, size_t path_len, struct mg_file_stat *filestat) { const char *list = conn->dom_ctx->config[INDEX_FILES]; struct vec filename_vec; size_t n = strlen(path); int found = 0; /* The 'path' given to us points to the directory. Remove all trailing * directory separator characters from the end of the path, and * then append single directory separator character. */ while ((n > 0) && (path[n - 1] == '/')) { n--; } path[n] = '/'; /* Traverse index files list. For each entry, append it to the given * path and see if the file exists. If it exists, break the loop */ while ((list = next_option(list, &filename_vec, NULL)) != NULL) { /* Ignore too long entries that may overflow path buffer */ if ((filename_vec.len + 1) > (path_len - (n + 1))) { continue; } /* Prepare full path to the index file */ mg_strlcpy(path + n + 1, filename_vec.ptr, filename_vec.len + 1); /* Does it exist? */ if (mg_stat(conn, path, filestat)) { /* Yes it does, break the loop */ found = 1; break; } } /* If no index file exists, restore directory path */ if (!found) { path[n] = '\0'; } return found; } #endif static void interpret_uri(struct mg_connection *conn, /* in/out: request (must be valid) */ char *filename, /* out: filename */ size_t filename_buf_len, /* in: size of filename buffer */ struct mg_file_stat *filestat, /* out: file status structure */ int *is_found, /* out: file found (directly) */ int *is_script_resource, /* out: handled by a script? */ int *is_websocket_request, /* out: websocket connection? */ int *is_put_or_delete_request, /* out: put/delete a file? */ int *is_webdav_request, /* out: webdav request? */ int *is_template_text /* out: SSI file or LSP file? */ ) { char const *accept_encoding; #if !defined(NO_FILES) const char *uri = conn->request_info.local_uri; const char *root = conn->dom_ctx->config[DOCUMENT_ROOT]; const char *rewrite; struct vec a, b; ptrdiff_t match_len; char gz_path[UTF8_PATH_MAX]; int truncated; #if !defined(NO_CGI) || defined(USE_LUA) || defined(USE_DUKTAPE) char *tmp_str; size_t tmp_str_len, sep_pos; int allow_substitute_script_subresources; #endif #else (void)filename_buf_len; /* unused if NO_FILES is defined */ #endif /* Step 1: Set all initially unknown outputs to zero */ memset(filestat, 0, sizeof(*filestat)); *filename = 0; *is_found = 0; *is_script_resource = 0; *is_template_text = 0; /* Step 2: Classify the request method */ /* Step 2a: Check if the request attempts to modify the file system */ *is_put_or_delete_request = is_put_or_delete_method(conn); /* Step 2b: Check if the request uses WebDav method that requires special * handling */ *is_webdav_request = is_civetweb_webdav_method(conn); /* Step 3: Check if it is a websocket request, and modify the document * root if required */ #if defined(USE_WEBSOCKET) *is_websocket_request = (conn->protocol_type == PROTOCOL_TYPE_WEBSOCKET); #if !defined(NO_FILES) if ((*is_websocket_request) && conn->dom_ctx->config[WEBSOCKET_ROOT]) { root = conn->dom_ctx->config[WEBSOCKET_ROOT]; } #endif /* !NO_FILES */ #else /* USE_WEBSOCKET */ *is_websocket_request = 0; #endif /* USE_WEBSOCKET */ /* Step 4: Check if gzip encoded response is allowed */ conn->accept_gzip = 0; if ((accept_encoding = mg_get_header(conn, "Accept-Encoding")) != NULL) { if (strstr(accept_encoding, "gzip") != NULL) { conn->accept_gzip = 1; } } #if !defined(NO_FILES) /* Step 5: If there is no root directory, don't look for files. */ /* Note that root == NULL is a regular use case here. This occurs, * if all requests are handled by callbacks, so the WEBSOCKET_ROOT * config is not required. */ if (root == NULL) { /* all file related outputs have already been set to 0, just return */ return; } /* Step 6: Determine the local file path from the root path and the * request uri. */ /* Using filename_buf_len - 1 because memmove() for PATH_INFO may shift * part of the path one byte on the right. */ truncated = 0; mg_snprintf( conn, &truncated, filename, filename_buf_len - 1, "%s%s", root, uri); if (truncated) { goto interpret_cleanup; } /* Step 7: URI rewriting */ rewrite = conn->dom_ctx->config[URL_REWRITE_PATTERN]; while ((rewrite = next_option(rewrite, &a, &b)) != NULL) { if ((match_len = match_prefix(a.ptr, a.len, uri)) > 0) { mg_snprintf(conn, &truncated, filename, filename_buf_len - 1, "%.*s%s", (int)b.len, b.ptr, uri + match_len); break; } } if (truncated) { goto interpret_cleanup; } /* Step 8: Check if the file exists at the server */ /* Local file path and name, corresponding to requested URI * is now stored in "filename" variable. */ if (mg_stat(conn, filename, filestat)) { int uri_len = (int)strlen(uri); int is_uri_end_slash = (uri_len > 0) && (uri[uri_len - 1] == '/'); /* 8.1: File exists. */ *is_found = 1; /* 8.2: Check if it is a script type. */ if (extention_matches_script(conn, filename)) { /* The request addresses a CGI resource, Lua script or * server-side javascript. * The URI corresponds to the script itself (like * /path/script.cgi), and there is no additional resource * path (like /path/script.cgi/something). * Requests that modify (replace or delete) a resource, like * PUT and DELETE requests, should replace/delete the script * file. * Requests that read or write from/to a resource, like GET and * POST requests, should call the script and return the * generated response. */ *is_script_resource = (!*is_put_or_delete_request); } /* 8.3: Check for SSI and LSP files */ if (extention_matches_template_text(conn, filename)) { /* Same as above, but for *.lsp and *.shtml files. */ /* A "template text" is a file delivered directly to the client, * but with some text tags replaced by dynamic content. * E.g. a Server Side Include (SSI) or Lua Page/Lua Server Page * (LP, LSP) file. */ *is_template_text = (!*is_put_or_delete_request); } /* 8.4: If the request target is a directory, there could be * a substitute file (index.html, index.cgi, ...). */ /* But do not substitute a directory for a WebDav request */ if (filestat->is_directory && is_uri_end_slash && (!*is_webdav_request)) { /* Use a local copy here, since substitute_index_file will * change the content of the file status */ struct mg_file_stat tmp_filestat; memset(&tmp_filestat, 0, sizeof(tmp_filestat)); if (substitute_index_file( conn, filename, filename_buf_len, &tmp_filestat)) { /* Substitute file found. Copy stat to the output, then * check if the file is a script file */ *filestat = tmp_filestat; if (extention_matches_script(conn, filename)) { /* Substitute file is a script file */ *is_script_resource = 1; } else if (extention_matches_template_text(conn, filename)) { /* Substitute file is a LSP or SSI file */ *is_template_text = 1; } else { /* Substitute file is a regular file */ *is_script_resource = 0; *is_found = (mg_stat(conn, filename, filestat) ? 1 : 0); } } /* If there is no substitute file, the server could return * a directory listing in a later step */ } return; } /* Step 9: Check for zipped files: */ /* If we can't find the actual file, look for the file * with the same name but a .gz extension. If we find it, * use that and set the gzipped flag in the file struct * to indicate that the response need to have the content- * encoding: gzip header. * We can only do this if the browser declares support. */ if (conn->accept_gzip) { mg_snprintf( conn, &truncated, gz_path, sizeof(gz_path), "%s.gz", filename); if (truncated) { goto interpret_cleanup; } if (mg_stat(conn, gz_path, filestat)) { if (filestat) { filestat->is_gzipped = 1; *is_found = 1; } /* Currently gz files can not be scripts. */ return; } } #if !defined(NO_CGI) || defined(USE_LUA) || defined(USE_DUKTAPE) /* Step 10: Script resources may handle sub-resources */ /* Support PATH_INFO for CGI scripts. */ tmp_str_len = strlen(filename); tmp_str = (char *)mg_malloc_ctx(tmp_str_len + UTF8_PATH_MAX + 1, conn->phys_ctx); if (!tmp_str) { /* Out of memory */ goto interpret_cleanup; } memcpy(tmp_str, filename, tmp_str_len + 1); /* Check config, if index scripts may have sub-resources */ allow_substitute_script_subresources = !mg_strcasecmp(conn->dom_ctx->config[ALLOW_INDEX_SCRIPT_SUB_RES], "yes"); if (*is_webdav_request) { /* TO BE DEFINED: Should scripts handle special WebDAV methods lile * PROPFIND for their subresources? */ /* allow_substitute_script_subresources = 0; */ } sep_pos = tmp_str_len; while (sep_pos > 0) { sep_pos--; if (tmp_str[sep_pos] == '/') { int is_script = 0, does_exist = 0; tmp_str[sep_pos] = 0; if (tmp_str[0]) { is_script = extention_matches_script(conn, tmp_str); does_exist = mg_stat(conn, tmp_str, filestat); } if (does_exist && is_script) { filename[sep_pos] = 0; memmove(filename + sep_pos + 2, filename + sep_pos + 1, strlen(filename + sep_pos + 1) + 1); conn->path_info = filename + sep_pos + 1; filename[sep_pos + 1] = '/'; *is_script_resource = 1; *is_found = 1; break; } if (allow_substitute_script_subresources) { if (substitute_index_file( conn, tmp_str, tmp_str_len + UTF8_PATH_MAX, filestat)) { /* some intermediate directory has an index file */ if (extention_matches_script(conn, tmp_str)) { size_t script_name_len = strlen(tmp_str); /* subres_name read before this memory locatio will be overwritten */ char *subres_name = filename + sep_pos; size_t subres_name_len = strlen(subres_name); DEBUG_TRACE("Substitute script %s serving path %s", tmp_str, filename); /* this index file is a script */ if ((script_name_len + subres_name_len + 2) >= filename_buf_len) { mg_free(tmp_str); goto interpret_cleanup; } conn->path_info = filename + script_name_len + 1; /* new target */ memmove(conn->path_info, subres_name, subres_name_len); conn->path_info[subres_name_len] = 0; memcpy(filename, tmp_str, script_name_len + 1); *is_script_resource = 1; *is_found = 1; break; } else { DEBUG_TRACE("Substitute file %s serving path %s", tmp_str, filename); /* non-script files will not have sub-resources */ filename[sep_pos] = 0; conn->path_info = 0; *is_script_resource = 0; *is_found = 0; break; } } } tmp_str[sep_pos] = '/'; } } mg_free(tmp_str); #endif /* !defined(NO_CGI) || defined(USE_LUA) || defined(USE_DUKTAPE) */ #endif /* !defined(NO_FILES) */ return; #if !defined(NO_FILES) /* Reset all outputs */ interpret_cleanup: memset(filestat, 0, sizeof(*filestat)); *filename = 0; *is_found = 0; *is_script_resource = 0; *is_websocket_request = 0; *is_put_or_delete_request = 0; #endif /* !defined(NO_FILES) */ } /* Check whether full request is buffered. Return: * -1 if request or response is malformed * 0 if request or response is not yet fully buffered * >0 actual request length, including last \r\n\r\n */ static int get_http_header_len(const char *buf, int buflen) { int i; for (i = 0; i < buflen; i++) { /* Do an unsigned comparison in some conditions below */ const unsigned char c = (unsigned char)buf[i]; if ((c < 128) && ((char)c != '\r') && ((char)c != '\n') && !isprint(c)) { /* abort scan as soon as one malformed character is found */ return -1; } if (i < buflen - 1) { if ((buf[i] == '\n') && (buf[i + 1] == '\n')) { /* Two newline, no carriage return - not standard compliant, * but it should be accepted */ return i + 2; } } if (i < buflen - 3) { if ((buf[i] == '\r') && (buf[i + 1] == '\n') && (buf[i + 2] == '\r') && (buf[i + 3] == '\n')) { /* Two \r\n - standard compliant */ return i + 4; } } } return 0; } #if !defined(NO_CACHING) /* Convert month to the month number. Return -1 on error, or month number */ static int get_month_index(const char *s) { size_t i; for (i = 0; i < ARRAY_SIZE(month_names); i++) { if (!strcmp(s, month_names[i])) { return (int)i; } } return -1; } /* Parse UTC date-time string, and return the corresponding time_t value. */ static time_t parse_date_string(const char *datetime) { char month_str[32] = {0}; int second, minute, hour, day, month, year; time_t result = (time_t)0; struct tm tm; if ((sscanf(datetime, "%d/%3s/%d %d:%d:%d", &day, month_str, &year, &hour, &minute, &second) == 6) || (sscanf(datetime, "%d %3s %d %d:%d:%d", &day, month_str, &year, &hour, &minute, &second) == 6) || (sscanf(datetime, "%*3s, %d %3s %d %d:%d:%d", &day, month_str, &year, &hour, &minute, &second) == 6) || (sscanf(datetime, "%d-%3s-%d %d:%d:%d", &day, month_str, &year, &hour, &minute, &second) == 6)) { month = get_month_index(month_str); if ((month >= 0) && (year >= 1970)) { memset(&tm, 0, sizeof(tm)); tm.tm_year = year - 1900; tm.tm_mon = month; tm.tm_mday = day; tm.tm_hour = hour; tm.tm_min = minute; tm.tm_sec = second; result = timegm(&tm); } } return result; } #endif /* !NO_CACHING */ /* Pre-process URIs according to RFC + protect against directory disclosure * attacks by removing '..', excessive '/' and '\' characters */ static void remove_dot_segments(char *inout) { /* Windows backend protection * (https://tools.ietf.org/html/rfc3986#section-7.3): Replace backslash * in URI by slash */ char *out_end = inout; char *in = inout; if (!in) { /* Param error. */ return; } while (*in) { if (*in == '\\') { *in = '/'; } in++; } /* Algorithm "remove_dot_segments" from * https://tools.ietf.org/html/rfc3986#section-5.2.4 */ /* Step 1: * The input buffer is initialized. * The output buffer is initialized to the empty string. */ in = inout; /* Step 2: * While the input buffer is not empty, loop as follows: */ /* Less than out_end of the inout buffer is used as output, so keep * condition: out_end <= in */ while (*in) { /* Step 2a: * If the input buffer begins with a prefix of "../" or "./", * then remove that prefix from the input buffer; */ if (!strncmp(in, "../", 3)) { in += 3; } else if (!strncmp(in, "./", 2)) { in += 2; } /* otherwise */ /* Step 2b: * if the input buffer begins with a prefix of "/./" or "/.", * where "." is a complete path segment, then replace that * prefix with "/" in the input buffer; */ else if (!strncmp(in, "/./", 3)) { in += 2; } else if (!strcmp(in, "/.")) { in[1] = 0; } /* otherwise */ /* Step 2c: * if the input buffer begins with a prefix of "/../" or "/..", * where ".." is a complete path segment, then replace that * prefix with "/" in the input buffer and remove the last * segment and its preceding "/" (if any) from the output * buffer; */ else if (!strncmp(in, "/../", 4)) { in += 3; if (inout != out_end) { /* remove last segment */ do { out_end--; } while ((inout != out_end) && (*out_end != '/')); } } else if (!strcmp(in, "/..")) { in[1] = 0; if (inout != out_end) { /* remove last segment */ do { out_end--; } while ((inout != out_end) && (*out_end != '/')); } } /* otherwise */ /* Step 2d: * if the input buffer consists only of "." or "..", then remove * that from the input buffer; */ else if (!strcmp(in, ".") || !strcmp(in, "..")) { *in = 0; } /* otherwise */ /* Step 2e: * move the first path segment in the input buffer to the end of * the output buffer, including the initial "/" character (if * any) and any subsequent characters up to, but not including, * the next "/" character or the end of the input buffer. */ else { do { *out_end = *in; out_end++; in++; } while ((*in != 0) && (*in != '/')); } } /* Step 3: * Finally, the output buffer is returned as the result of * remove_dot_segments. */ /* Terminate output */ *out_end = 0; /* For Windows, the files/folders "x" and "x." (with a dot but without * extension) are identical. Replace all "./" by "/" and remove a "." at * the end. Also replace all "//" by "/". Repeat until there is no "./" * or "//" anymore. */ out_end = in = inout; while (*in) { if (*in == '.') { /* remove . at the end or preceding of / */ char *in_ahead = in; do { in_ahead++; } while (*in_ahead == '.'); if (*in_ahead == '/') { in = in_ahead; if ((out_end != inout) && (out_end[-1] == '/')) { /* remove generated // */ out_end--; } } else if (*in_ahead == 0) { in = in_ahead; } else { do { *out_end++ = '.'; in++; } while (in != in_ahead); } } else if (*in == '/') { /* replace // by / */ *out_end++ = '/'; do { in++; } while (*in == '/'); } else { *out_end++ = *in; in++; } } *out_end = 0; } static const struct { const char *extension; size_t ext_len; const char *mime_type; } builtin_mime_types[] = { /* IANA registered MIME types * (http://www.iana.org/assignments/media-types) * application types */ {".bin", 4, "application/octet-stream"}, {".deb", 4, "application/octet-stream"}, {".dmg", 4, "application/octet-stream"}, {".dll", 4, "application/octet-stream"}, {".doc", 4, "application/msword"}, {".eps", 4, "application/postscript"}, {".exe", 4, "application/octet-stream"}, {".iso", 4, "application/octet-stream"}, {".js", 3, "application/javascript"}, {".json", 5, "application/json"}, {".msi", 4, "application/octet-stream"}, {".pdf", 4, "application/pdf"}, {".ps", 3, "application/postscript"}, {".rtf", 4, "application/rtf"}, {".xhtml", 6, "application/xhtml+xml"}, {".xsl", 4, "application/xml"}, {".xslt", 5, "application/xml"}, /* fonts */ {".ttf", 4, "application/font-sfnt"}, {".cff", 4, "application/font-sfnt"}, {".otf", 4, "application/font-sfnt"}, {".aat", 4, "application/font-sfnt"}, {".sil", 4, "application/font-sfnt"}, {".pfr", 4, "application/font-tdpfr"}, {".woff", 5, "application/font-woff"}, {".woff2", 6, "application/font-woff2"}, /* audio */ {".mp3", 4, "audio/mpeg"}, {".oga", 4, "audio/ogg"}, {".ogg", 4, "audio/ogg"}, /* image */ {".gif", 4, "image/gif"}, {".ief", 4, "image/ief"}, {".jpeg", 5, "image/jpeg"}, {".jpg", 4, "image/jpeg"}, {".jpm", 4, "image/jpm"}, {".jpx", 4, "image/jpx"}, {".png", 4, "image/png"}, {".svg", 4, "image/svg+xml"}, {".tif", 4, "image/tiff"}, {".tiff", 5, "image/tiff"}, /* model */ {".wrl", 4, "model/vrml"}, /* text */ {".css", 4, "text/css"}, {".csv", 4, "text/csv"}, {".htm", 4, "text/html"}, {".html", 5, "text/html"}, {".sgm", 4, "text/sgml"}, {".shtm", 5, "text/html"}, {".shtml", 6, "text/html"}, {".txt", 4, "text/plain"}, {".xml", 4, "text/xml"}, /* video */ {".mov", 4, "video/quicktime"}, {".mp4", 4, "video/mp4"}, {".mpeg", 5, "video/mpeg"}, {".mpg", 4, "video/mpeg"}, {".ogv", 4, "video/ogg"}, {".qt", 3, "video/quicktime"}, /* not registered types * (http://reference.sitepoint.com/html/mime-types-full, * http://www.hansenb.pdx.edu/DMKB/dict/tutorials/mime_typ.php, ..) */ {".arj", 4, "application/x-arj-compressed"}, {".gz", 3, "application/x-gunzip"}, {".rar", 4, "application/x-arj-compressed"}, {".swf", 4, "application/x-shockwave-flash"}, {".tar", 4, "application/x-tar"}, {".tgz", 4, "application/x-tar-gz"}, {".torrent", 8, "application/x-bittorrent"}, {".ppt", 4, "application/x-mspowerpoint"}, {".xls", 4, "application/x-msexcel"}, {".zip", 4, "application/x-zip-compressed"}, {".aac", 4, "audio/aac"}, /* http://en.wikipedia.org/wiki/Advanced_Audio_Coding */ {".flac", 5, "audio/flac"}, {".aif", 4, "audio/x-aif"}, {".m3u", 4, "audio/x-mpegurl"}, {".mid", 4, "audio/x-midi"}, {".ra", 3, "audio/x-pn-realaudio"}, {".ram", 4, "audio/x-pn-realaudio"}, {".wav", 4, "audio/x-wav"}, {".bmp", 4, "image/bmp"}, {".ico", 4, "image/x-icon"}, {".pct", 4, "image/x-pct"}, {".pict", 5, "image/pict"}, {".rgb", 4, "image/x-rgb"}, {".webm", 5, "video/webm"}, /* http://en.wikipedia.org/wiki/WebM */ {".asf", 4, "video/x-ms-asf"}, {".avi", 4, "video/x-msvideo"}, {".m4v", 4, "video/x-m4v"}, {NULL, 0, NULL}}; CIVETWEB_API const char * mg_get_builtin_mime_type(const char *path) { const char *ext; size_t i, path_len; path_len = strlen(path); for (i = 0; builtin_mime_types[i].extension != NULL; i++) { ext = path + (path_len - builtin_mime_types[i].ext_len); if ((path_len > builtin_mime_types[i].ext_len) && (mg_strcasecmp(ext, builtin_mime_types[i].extension) == 0)) { return builtin_mime_types[i].mime_type; } } return "text/plain"; } /* Look at the "path" extension and figure what mime type it has. * Store mime type in the vector. */ static void get_mime_type(struct mg_connection *conn, const char *path, struct vec *vec) { struct vec ext_vec, mime_vec; const char *list, *ext; size_t path_len; path_len = strlen(path); if ((conn == NULL) || (vec == NULL)) { if (vec != NULL) { memset(vec, '\0', sizeof(struct vec)); } return; } /* Scan user-defined mime types first, in case user wants to * override default mime types. */ list = conn->dom_ctx->config[EXTRA_MIME_TYPES]; while ((list = next_option(list, &ext_vec, &mime_vec)) != NULL) { /* ext now points to the path suffix */ ext = path + path_len - ext_vec.len; if (mg_strncasecmp(ext, ext_vec.ptr, ext_vec.len) == 0) { *vec = mime_vec; return; } } vec->ptr = mg_get_builtin_mime_type(path); vec->len = strlen(vec->ptr); } /* Stringify binary data. Output buffer must be twice as big as input, * because each byte takes 2 bytes in string representation */ static void bin2str(char *to, const unsigned char *p, size_t len) { static const char *hex = "0123456789abcdef"; for (; len--; p++) { *to++ = hex[p[0] >> 4]; *to++ = hex[p[0] & 0x0f]; } *to = '\0'; } /* Return stringified MD5 hash for list of strings. Buffer must be 33 bytes. */ CIVETWEB_API char * mg_md5(char buf[33], ...) { md5_byte_t hash[16]; const char *p; va_list ap; md5_state_t ctx; md5_init(&ctx); va_start(ap, buf); while ((p = va_arg(ap, const char *)) != NULL) { md5_append(&ctx, (const md5_byte_t *)p, strlen(p)); } va_end(ap); md5_finish(&ctx, hash); bin2str(buf, hash, sizeof(hash)); return buf; } /* Check the user's password, return 1 if OK */ static int check_password_digest(const char *method, const char *ha1, const char *uri, const char *nonce, const char *nc, const char *cnonce, const char *qop, const char *response) { char ha2[32 + 1], expected_response[32 + 1]; /* Some of the parameters may be NULL */ if ((method == NULL) || (nonce == NULL) || (nc == NULL) || (cnonce == NULL) || (qop == NULL) || (response == NULL)) { return 0; } /* NOTE(lsm): due to a bug in MSIE, we do not compare the URI */ if (strlen(response) != 32) { return 0; } mg_md5(ha2, method, ":", uri, NULL); mg_md5(expected_response, ha1, ":", nonce, ":", nc, ":", cnonce, ":", qop, ":", ha2, NULL); return mg_strcasecmp(response, expected_response) == 0; } #if !defined(NO_FILESYSTEMS) /* Use the global passwords file, if specified by auth_gpass option, * or search for .htpasswd in the requested directory. */ static void open_auth_file(struct mg_connection *conn, const char *path, struct mg_file *filep) { if ((conn != NULL) && (conn->dom_ctx != NULL)) { char name[UTF8_PATH_MAX]; const char *p, *e, *gpass = conn->dom_ctx->config[GLOBAL_PASSWORDS_FILE]; int truncated; if (gpass != NULL) { /* Use global passwords file */ if (!mg_fopen(conn, gpass, MG_FOPEN_MODE_READ, filep)) { #if defined(DEBUG) /* Use mg_cry_internal here, since gpass has been * configured. */ mg_cry_internal(conn, "fopen(%s): %s", gpass, strerror(ERRNO)); #endif } /* Important: using local struct mg_file to test path for * is_directory flag. If filep is used, mg_stat() makes it * appear as if auth file was opened. * TODO(mid): Check if this is still required after rewriting * mg_stat */ } else if (mg_stat(conn, path, &filep->stat) && filep->stat.is_directory) { mg_snprintf(conn, &truncated, name, sizeof(name), "%s/%s", path, PASSWORDS_FILE_NAME); if (truncated || !mg_fopen(conn, name, MG_FOPEN_MODE_READ, filep)) { #if defined(DEBUG) /* Don't use mg_cry_internal here, but only a trace, since * this is a typical case. It will occur for every directory * without a password file. */ DEBUG_TRACE("fopen(%s): %s", name, strerror(ERRNO)); #endif } } else { /* Try to find .htpasswd in requested directory. */ for (p = path, e = p + strlen(p) - 1; e > p; e--) { if (e[0] == '/') { break; } } mg_snprintf(conn, &truncated, name, sizeof(name), "%.*s/%s", (int)(e - p), p, PASSWORDS_FILE_NAME); if (truncated || !mg_fopen(conn, name, MG_FOPEN_MODE_READ, filep)) { #if defined(DEBUG) /* Don't use mg_cry_internal here, but only a trace, since * this is a typical case. It will occur for every directory * without a password file. */ DEBUG_TRACE("fopen(%s): %s", name, strerror(ERRNO)); #endif } } } } #endif /* NO_FILESYSTEMS */ /* Parsed Authorization header */ struct ah { char *user; int type; /* 1 = basic, 2 = digest */ char *plain_password; /* Basic only */ char *uri, *cnonce, *response, *qop, *nc, *nonce; /* Digest only */ }; /* Return 1 on success. Always initializes the ah structure. */ static int parse_auth_header(struct mg_connection *conn, char *buf, size_t buf_size, struct ah *ah) { char *name, *value, *s; const char *auth_header; uint64_t nonce; if (!ah || !conn) { return 0; } (void)memset(ah, 0, sizeof(*ah)); auth_header = mg_get_header(conn, "Authorization"); if (auth_header == NULL) { /* No Authorization header at all */ return 0; } if (0 == mg_strncasecmp(auth_header, "Basic ", 6)) { /* Basic Auth (we never asked for this, but some client may send it) */ char *split; const char *userpw_b64 = auth_header + 6; size_t userpw_b64_len = strlen(userpw_b64); size_t buf_len_r = buf_size; if (mg_base64_decode( userpw_b64, userpw_b64_len, (unsigned char *)buf, &buf_len_r) != -1) { return 0; /* decode error */ } split = strchr(buf, ':'); if (!split) { return 0; /* Format error */ } /* Separate string at ':' */ *split = 0; /* User name is before ':', Password is after ':' */ ah->user = buf; ah->type = 1; ah->plain_password = split + 1; return 1; } else if (0 == mg_strncasecmp(auth_header, "Digest ", 7)) { /* Digest Auth ... implemented below */ ah->type = 2; } else { /* Unknown or invalid Auth method */ return 0; } /* Make modifiable copy of the auth header */ (void)mg_strlcpy(buf, auth_header + 7, buf_size); s = buf; /* Parse authorization header */ for (;;) { /* Gobble initial spaces */ while (isspace((unsigned char)*s)) { s++; } name = skip_quoted(&s, "=", " ", 0); /* Value is either quote-delimited, or ends at first comma or space. */ if (s[0] == '\"') { s++; value = skip_quoted(&s, "\"", " ", '\\'); if (s[0] == ',') { s++; } } else { value = skip_quoted(&s, ", ", " ", 0); /* IE uses commas, FF * uses spaces */ } if (*name == '\0') { break; } if (!strcmp(name, "username")) { ah->user = value; } else if (!strcmp(name, "cnonce")) { ah->cnonce = value; } else if (!strcmp(name, "response")) { ah->response = value; } else if (!strcmp(name, "uri")) { ah->uri = value; } else if (!strcmp(name, "qop")) { ah->qop = value; } else if (!strcmp(name, "nc")) { ah->nc = value; } else if (!strcmp(name, "nonce")) { ah->nonce = value; } } #if !defined(NO_NONCE_CHECK) /* Read the nonce from the response. */ if (ah->nonce == NULL) { return 0; } s = NULL; nonce = strtoull(ah->nonce, &s, 10); if ((s == NULL) || (*s != 0)) { return 0; } /* Convert the nonce from the client to a number. */ nonce ^= conn->dom_ctx->auth_nonce_mask; /* The converted number corresponds to the time the nounce has been * created. This should not be earlier than the server start. */ /* Server side nonce check is valuable in all situations but one: * if the server restarts frequently, but the client should not see * that, so the server should accept nonces from previous starts. */ /* However, the reasonable default is to not accept a nonce from a * previous start, so if anyone changed the access rights between * two restarts, a new login is required. */ if (nonce < (uint64_t)conn->phys_ctx->start_time) { /* nonce is from a previous start of the server and no longer valid * (replay attack?) */ return 0; } /* Check if the nonce is too high, so it has not (yet) been used by the * server. */ if (nonce >= ((uint64_t)conn->phys_ctx->start_time + conn->dom_ctx->nonce_count)) { return 0; } #else (void)nonce; #endif return (ah->user != NULL); } static const char * mg_fgets(char *buf, size_t size, struct mg_file *filep) { if (!filep) { return NULL; } if (filep->access.fp != NULL) { return fgets(buf, (int)size, filep->access.fp); } else { return NULL; } } /* Define the initial recursion depth for procesesing htpasswd files that * include other htpasswd * (or even the same) files. It is not difficult to provide a file or files * s.t. they force civetweb * to infinitely recurse and then crash. */ #define INITIAL_DEPTH 9 #if INITIAL_DEPTH <= 0 #error Bad INITIAL_DEPTH for recursion, set to at least 1 #endif #if !defined(NO_FILESYSTEMS) struct read_auth_file_struct { struct mg_connection *conn; struct ah ah; const char *domain; char buf[256 + 256 + 40]; const char *f_user; const char *f_domain; const char *f_ha1; }; static int read_auth_file(struct mg_file *filep, struct read_auth_file_struct *workdata, int depth) { int is_authorized = 0; struct mg_file fp; size_t l; if (!filep || !workdata || (0 == depth)) { return 0; } /* Loop over passwords file */ while (mg_fgets(workdata->buf, sizeof(workdata->buf), filep) != NULL) { l = strlen(workdata->buf); while (l > 0) { if (isspace((unsigned char)workdata->buf[l - 1]) || iscntrl((unsigned char)workdata->buf[l - 1])) { l--; workdata->buf[l] = 0; } else break; } if (l < 1) { continue; } workdata->f_user = workdata->buf; if (workdata->f_user[0] == ':') { /* user names may not contain a ':' and may not be empty, * so lines starting with ':' may be used for a special purpose */ if (workdata->f_user[1] == '#') { /* :# is a comment */ continue; } else if (!strncmp(workdata->f_user + 1, "include=", 8)) { if (mg_fopen(workdata->conn, workdata->f_user + 9, MG_FOPEN_MODE_READ, &fp)) { is_authorized = read_auth_file(&fp, workdata, depth - 1); (void)mg_fclose( &fp.access); /* ignore error on read only file */ /* No need to continue processing files once we have a * match, since nothing will reset it back * to 0. */ if (is_authorized) { return is_authorized; } } else { mg_cry_internal(workdata->conn, "%s: cannot open authorization file: %s", __func__, workdata->buf); } continue; } /* everything is invalid for the moment (might change in the * future) */ mg_cry_internal(workdata->conn, "%s: syntax error in authorization file: %s", __func__, workdata->buf); continue; } workdata->f_domain = strchr(workdata->f_user, ':'); if (workdata->f_domain == NULL) { mg_cry_internal(workdata->conn, "%s: syntax error in authorization file: %s", __func__, workdata->buf); continue; } *(char *)(workdata->f_domain) = 0; (workdata->f_domain)++; workdata->f_ha1 = strchr(workdata->f_domain, ':'); if (workdata->f_ha1 == NULL) { mg_cry_internal(workdata->conn, "%s: syntax error in authorization file: %s", __func__, workdata->buf); continue; } *(char *)(workdata->f_ha1) = 0; (workdata->f_ha1)++; if (!strcmp(workdata->ah.user, workdata->f_user) && !strcmp(workdata->domain, workdata->f_domain)) { switch (workdata->ah.type) { case 1: /* Basic */ { char md5[33]; mg_md5(md5, workdata->f_user, ":", workdata->domain, ":", workdata->ah.plain_password, NULL); return 0 == memcmp(workdata->f_ha1, md5, 33); } case 2: /* Digest */ return check_password_digest( workdata->conn->request_info.request_method, workdata->f_ha1, workdata->ah.uri, workdata->ah.nonce, workdata->ah.nc, workdata->ah.cnonce, workdata->ah.qop, workdata->ah.response); default: /* None/Other/Unknown */ return 0; } } } return is_authorized; } /* Authorize against the opened passwords file. Return 1 if authorized. */ static int authorize(struct mg_connection *conn, struct mg_file *filep, const char *realm) { struct read_auth_file_struct workdata; char buf[MG_BUF_LEN]; if (!conn || !conn->dom_ctx) { return 0; } memset(&workdata, 0, sizeof(workdata)); workdata.conn = conn; if (!parse_auth_header(conn, buf, sizeof(buf), &workdata.ah)) { return 0; } /* CGI needs it as REMOTE_USER */ conn->request_info.remote_user = mg_strdup_ctx(workdata.ah.user, conn->phys_ctx); if (realm) { workdata.domain = realm; } else { workdata.domain = conn->dom_ctx->config[AUTHENTICATION_DOMAIN]; } return read_auth_file(filep, &workdata, INITIAL_DEPTH); } /* Public function to check http digest authentication header */ CIVETWEB_API int mg_check_digest_access_authentication(struct mg_connection *conn, const char *realm, const char *filename) { struct mg_file file = STRUCT_FILE_INITIALIZER; int auth; if (!conn || !filename) { return -1; } if (!mg_fopen(conn, filename, MG_FOPEN_MODE_READ, &file)) { return -2; } auth = authorize(conn, &file, realm); mg_fclose(&file.access); return auth; } #endif /* NO_FILESYSTEMS */ /* Return 1 if request is authorised, 0 otherwise. */ static int check_authorization(struct mg_connection *conn, const char *path) { #if !defined(NO_FILESYSTEMS) char fname[UTF8_PATH_MAX]; struct vec uri_vec, filename_vec; const char *list; struct mg_file file = STRUCT_FILE_INITIALIZER; int authorized = 1, truncated; if (!conn || !conn->dom_ctx) { return 0; } list = conn->dom_ctx->config[PROTECT_URI]; while ((list = next_option(list, &uri_vec, &filename_vec)) != NULL) { if (!memcmp(conn->request_info.local_uri, uri_vec.ptr, uri_vec.len)) { mg_snprintf(conn, &truncated, fname, sizeof(fname), "%.*s", (int)filename_vec.len, filename_vec.ptr); if (truncated || !mg_fopen(conn, fname, MG_FOPEN_MODE_READ, &file)) { mg_cry_internal(conn, "%s: cannot open %s: %s", __func__, fname, strerror(errno)); } break; } } if (!is_file_opened(&file.access)) { open_auth_file(conn, path, &file); } if (is_file_opened(&file.access)) { authorized = authorize(conn, &file, NULL); (void)mg_fclose(&file.access); /* ignore error on read only file */ } return authorized; #else (void)conn; (void)path; return 1; #endif /* NO_FILESYSTEMS */ } /* Internal function. Assumes conn is valid */ static void send_authorization_request(struct mg_connection *conn, const char *realm) { uint64_t nonce = (uint64_t)(conn->phys_ctx->start_time); int trunc = 0; char buf[128]; if (!realm) { realm = conn->dom_ctx->config[AUTHENTICATION_DOMAIN]; } mg_lock_context(conn->phys_ctx); nonce += conn->dom_ctx->nonce_count; ++conn->dom_ctx->nonce_count; mg_unlock_context(conn->phys_ctx); nonce ^= conn->dom_ctx->auth_nonce_mask; conn->must_close = 1; /* Create 401 response */ mg_response_header_start(conn, 401); send_no_cache_header(conn); send_additional_header(conn); mg_response_header_add(conn, "Content-Length", "0", -1); /* Content for "WWW-Authenticate" header */ mg_snprintf(conn, &trunc, buf, sizeof(buf), "Digest qop=\"auth\", realm=\"%s\", " "nonce=\"%" UINT64_FMT "\"", realm, nonce); if (!trunc) { /* !trunc should always be true */ mg_response_header_add(conn, "WWW-Authenticate", buf, -1); } /* Send all headers */ mg_response_header_send(conn); } /* Interface function. Parameters are provided by the user, so do * at least some basic checks. */ CIVETWEB_API int mg_send_digest_access_authentication_request(struct mg_connection *conn, const char *realm) { if (conn && conn->dom_ctx) { send_authorization_request(conn, realm); return 0; } return -1; } #if !defined(NO_FILES) static int is_authorized_for_put(struct mg_connection *conn) { int ret = 0; if (conn) { struct mg_file file = STRUCT_FILE_INITIALIZER; const char *passfile = conn->dom_ctx->config[PUT_DELETE_PASSWORDS_FILE]; if (passfile != NULL && mg_fopen(conn, passfile, MG_FOPEN_MODE_READ, &file)) { ret = authorize(conn, &file, NULL); (void)mg_fclose(&file.access); /* ignore error on read only file */ } } DEBUG_TRACE("file write authorization: %i", ret); return ret; } #endif CIVETWEB_API int mg_modify_passwords_file_ha1(const char *fname, const char *domain, const char *user, const char *ha1) { int found = 0, i, result = 1; char line[512], u[256], d[256], h[256]; struct stat st = {0}; FILE *fp = NULL; char *temp_file = NULL; int temp_file_offs = 0; /* Regard empty password as no password - remove user record. */ if ((ha1 != NULL) && (ha1[0] == '\0')) { ha1 = NULL; } /* Other arguments must not be empty */ if ((fname == NULL) || (domain == NULL) || (user == NULL)) { return 0; } /* Using the given file format, user name and domain must not contain * the ':' character */ if (strchr(user, ':') != NULL) { return 0; } if (strchr(domain, ':') != NULL) { return 0; } /* Do not allow control characters like newline in user name and domain. * Do not allow excessively long names either. */ for (i = 0; ((i < 255) && (user[i] != 0)); i++) { if (iscntrl((unsigned char)user[i])) { return 0; } } if (user[i]) { return 0; /* user name too long */ } for (i = 0; ((i < 255) && (domain[i] != 0)); i++) { if (iscntrl((unsigned char)domain[i])) { return 0; } } if (domain[i]) { return 0; /* domain name too long */ } /* The maximum length of the path to the password file is limited */ if (strlen(fname) >= UTF8_PATH_MAX) { return 0; } /* Check if the file exists, and get file size */ if (0 == stat(fname, &st)) { int temp_buf_len; if (st.st_size > 10485760) { /* Some funster provided a >10 MB text file */ return 0; } /* Add enough space for one more line */ temp_buf_len = (int)st.st_size + 1024; /* Allocate memory (instead of using a temporary file) */ temp_file = (char *)mg_calloc((size_t)temp_buf_len, 1); if (!temp_file) { /* Out of memory */ return 0; } /* File exists. Read it into a memory buffer. */ fp = fopen(fname, "r"); if (fp == NULL) { /* Cannot read file. No permission? */ mg_free(temp_file); return 0; } /* Read content and store in memory */ while ((fgets(line, sizeof(line), fp) != NULL) && ((temp_file_offs + 600) < temp_buf_len)) { /* file format is "user:domain:hash\n" */ if (sscanf(line, "%255[^:]:%255[^:]:%255s", u, d, h) != 3) { continue; } u[255] = 0; d[255] = 0; h[255] = 0; if (!strcmp(u, user) && !strcmp(d, domain)) { /* Found the user: change the password hash or drop the user */ if ((ha1 != NULL) && (!found)) { i = snprintf(temp_file + temp_file_offs, temp_buf_len - temp_file_offs, "%s:%s:%s\n", user, domain, ha1); if (i < 1) { fclose(fp); mg_free(temp_file); return 0; } temp_file_offs += i; } found = 1; } else { /* Copy existing user, including password hash */ i = snprintf(temp_file + temp_file_offs, temp_buf_len - temp_file_offs, "%s:%s:%s\n", u, d, h); if (i < 1) { fclose(fp); mg_free(temp_file); return 0; } temp_file_offs += i; } } fclose(fp); } /* Create new file */ fp = fopen(fname, "w"); if (!fp) { mg_free(temp_file); return 0; } #if !defined(_WIN32) /* On Linux & co., restrict file read/write permissions to the owner */ if (fchmod(fileno(fp), S_IRUSR | S_IWUSR) != 0) { result = 0; } #endif if ((temp_file != NULL) && (temp_file_offs > 0)) { /* Store buffered content of old file */ if (fwrite(temp_file, 1, (size_t)temp_file_offs, fp) != (size_t)temp_file_offs) { result = 0; } } /* If new user, just add it */ if ((ha1 != NULL) && (!found)) { if (fprintf(fp, "%s:%s:%s\n", user, domain, ha1) < 6) { result = 0; } } /* All data written */ if (fclose(fp) != 0) { result = 0; } mg_free(temp_file); return result; } CIVETWEB_API int mg_modify_passwords_file(const char *fname, const char *domain, const char *user, const char *pass) { char ha1buf[33]; if ((fname == NULL) || (domain == NULL) || (user == NULL)) { return 0; } if ((pass == NULL) || (pass[0] == 0)) { return mg_modify_passwords_file_ha1(fname, domain, user, NULL); } mg_md5(ha1buf, user, ":", domain, ":", pass, NULL); return mg_modify_passwords_file_ha1(fname, domain, user, ha1buf); } static int is_valid_port(unsigned long port) { return (port <= 0xffff); } static int mg_inet_pton(int af, const char *src, void *dst, size_t dstlen, int resolve_src) { struct addrinfo hints, *res, *ressave; int func_ret = 0; int gai_ret; memset(&hints, 0, sizeof(struct addrinfo)); hints.ai_family = af; if (!resolve_src) { hints.ai_flags = AI_NUMERICHOST; } gai_ret = getaddrinfo(src, NULL, &hints, &res); if (gai_ret != 0) { /* gai_strerror could be used to convert gai_ret to a string */ /* POSIX return values: see * http://pubs.opengroup.org/onlinepubs/9699919799/functions/freeaddrinfo.html */ /* Windows return values: see * https://msdn.microsoft.com/en-us/library/windows/desktop/ms738520%28v=vs.85%29.aspx */ return 0; } ressave = res; while (res) { if ((dstlen >= (size_t)res->ai_addrlen) && (res->ai_addr->sa_family == af)) { memcpy(dst, res->ai_addr, res->ai_addrlen); func_ret = 1; } res = res->ai_next; } freeaddrinfo(ressave); return func_ret; } static int connect_socket( struct mg_context *ctx /* may be NULL */, const char *host, int port, /* 1..65535, or -99 for domain sockets (may be changed) */ int use_ssl, /* 0 or 1 */ struct mg_error_data *error, SOCKET *sock /* output: socket, must not be NULL */, union usa *sa /* output: socket address, must not be NULL */ ) { int ip_ver = 0; int conn_ret = -1; int sockerr = 0; *sock = INVALID_SOCKET; memset(sa, 0, sizeof(*sa)); if (host == NULL) { if (error != NULL) { error->code = MG_ERROR_DATA_CODE_INVALID_PARAM; mg_snprintf(NULL, NULL, /* No truncation check for ebuf */ error->text, error->text_buffer_size, "%s", "NULL host"); } return 0; } #if defined(USE_X_DOM_SOCKET) if (port == -99) { /* Unix domain socket */ size_t hostlen = strlen(host); if (hostlen >= sizeof(sa->sun.sun_path)) { if (error != NULL) { error->code = MG_ERROR_DATA_CODE_INVALID_PARAM; mg_snprintf(NULL, NULL, /* No truncation check for ebuf */ error->text, error->text_buffer_size, "%s", "host length exceeds limit"); } return 0; } } else #endif if ((port <= 0) || !is_valid_port((unsigned)port)) { if (error != NULL) { error->code = MG_ERROR_DATA_CODE_INVALID_PARAM; mg_snprintf(NULL, NULL, /* No truncation check for ebuf */ error->text, error->text_buffer_size, "%s", "invalid port"); } return 0; } #if !defined(NO_SSL) && !defined(USE_MBEDTLS) && !defined(NO_SSL_DL) #if defined(OPENSSL_API_1_1) || defined(OPENSSL_API_3_0) if (use_ssl && (TLS_client_method == NULL)) { if (error != NULL) { error->code = MG_ERROR_DATA_CODE_INIT_LIBRARY_FAILED; mg_snprintf(NULL, NULL, /* No truncation check for ebuf */ error->text, error->text_buffer_size, "%s", "SSL is not initialized"); } return 0; } #else if (use_ssl && (SSLv23_client_method == NULL)) { if (error != 0) { error->code = MG_ERROR_DATA_CODE_INIT_LIBRARY_FAILED; mg_snprintf(NULL, NULL, /* No truncation check for ebuf */ error->text, error->text_buffer_size, "%s", "SSL is not initialized"); } return 0; } #endif /* OPENSSL_API_1_1 || OPENSSL_API_3_0*/ #else (void)use_ssl; #endif /* NO SSL */ #if defined(USE_X_DOM_SOCKET) if (port == -99) { size_t hostlen = strlen(host); /* check (hostlen < sizeof(sun.sun_path)) already passed above */ ip_ver = -99; sa->sun.sun_family = AF_UNIX; memset(sa->sun.sun_path, 0, sizeof(sa->sun.sun_path)); memcpy(sa->sun.sun_path, host, hostlen); } else #endif if (mg_inet_pton(AF_INET, host, &sa->sin, sizeof(sa->sin), 1)) { sa->sin.sin_port = htons((uint16_t)port); ip_ver = 4; #if defined(USE_IPV6) } else if (mg_inet_pton(AF_INET6, host, &sa->sin6, sizeof(sa->sin6), 1)) { sa->sin6.sin6_port = htons((uint16_t)port); ip_ver = 6; } else if (host[0] == '[') { /* While getaddrinfo on Windows will work with [::1], * getaddrinfo on Linux only works with ::1 (without []). */ size_t l = strlen(host + 1); char *h = (l > 1) ? mg_strdup_ctx(host + 1, ctx) : NULL; if (h) { h[l - 1] = 0; if (mg_inet_pton(AF_INET6, h, &sa->sin6, sizeof(sa->sin6), 0)) { sa->sin6.sin6_port = htons((uint16_t)port); ip_ver = 6; } mg_free(h); } #endif } if (ip_ver == 0) { if (error != NULL) { error->code = MG_ERROR_DATA_CODE_HOST_NOT_FOUND; mg_snprintf(NULL, NULL, /* No truncation check for ebuf */ error->text, error->text_buffer_size, "%s", "host not found"); } return 0; } if (ip_ver == 4) { *sock = socket(PF_INET, SOCK_STREAM, 0); } #if defined(USE_IPV6) else if (ip_ver == 6) { *sock = socket(PF_INET6, SOCK_STREAM, 0); } #endif #if defined(USE_X_DOM_SOCKET) else if (ip_ver == -99) { *sock = socket(AF_UNIX, SOCK_STREAM, 0); } #endif if (*sock == INVALID_SOCKET) { if (error != NULL) { error->code = MG_ERROR_DATA_CODE_OS_ERROR; error->code_sub = (unsigned)ERRNO; mg_snprintf(NULL, NULL, /* No truncation check for ebuf */ error->text, error->text_buffer_size, "socket(): %s", strerror(ERRNO)); } return 0; } if (0 != set_non_blocking_mode(*sock)) { if (error != NULL) { error->code = MG_ERROR_DATA_CODE_OS_ERROR; error->code_sub = (unsigned)ERRNO; mg_snprintf(NULL, NULL, /* No truncation check for ebuf */ error->text, error->text_buffer_size, "Cannot set socket to non-blocking: %s", strerror(ERRNO)); } closesocket(*sock); *sock = INVALID_SOCKET; return 0; } set_close_on_exec(*sock, NULL, ctx); if (ip_ver == 4) { /* connected with IPv4 */ conn_ret = connect(*sock, (struct sockaddr *)((void *)&sa->sin), sizeof(sa->sin)); } #if defined(USE_IPV6) else if (ip_ver == 6) { /* connected with IPv6 */ conn_ret = connect(*sock, (struct sockaddr *)((void *)&sa->sin6), sizeof(sa->sin6)); } #endif #if defined(USE_X_DOM_SOCKET) else if (ip_ver == -99) { /* connected to domain socket */ conn_ret = connect(*sock, (struct sockaddr *)((void *)&sa->sun), sizeof(sa->sun)); } #endif if (conn_ret != 0) { sockerr = ERRNO; } #if defined(_WIN32) if ((conn_ret != 0) && (sockerr == WSAEWOULDBLOCK)) { #else if ((conn_ret != 0) && (sockerr == EINPROGRESS)) { #endif /* Data for getsockopt */ void *psockerr = &sockerr; int ret; #if defined(_WIN32) int len = (int)sizeof(sockerr); #else socklen_t len = (socklen_t)sizeof(sockerr); #endif /* Data for poll */ struct mg_pollfd pfd[1]; int pollres; int ms_wait = 10000; /* 10 second timeout */ stop_flag_t nonstop; STOP_FLAG_ASSIGN(&nonstop, 0); /* For a non-blocking socket, the connect sequence is: * 1) call connect (will not block) * 2) wait until the socket is ready for writing (select or poll) * 3) check connection state with getsockopt */ pfd[0].fd = *sock; pfd[0].events = POLLOUT; pollres = mg_poll(pfd, 1, ms_wait, ctx ? &(ctx->stop_flag) : &nonstop); if (pollres != 1) { /* Not connected */ if (error != NULL) { error->code = MG_ERROR_DATA_CODE_CONNECT_TIMEOUT; mg_snprintf(NULL, NULL, /* No truncation check for ebuf */ error->text, error->text_buffer_size, "connect(%s:%d): timeout", host, port); } closesocket(*sock); *sock = INVALID_SOCKET; return 0; } #if defined(_WIN32) ret = getsockopt(*sock, SOL_SOCKET, SO_ERROR, (char *)psockerr, &len); #else ret = getsockopt(*sock, SOL_SOCKET, SO_ERROR, psockerr, &len); #endif if ((ret == 0) && (sockerr == 0)) { conn_ret = 0; } } if (conn_ret != 0) { /* Not connected */ if (error != NULL) { error->code = MG_ERROR_DATA_CODE_CONNECT_FAILED; error->code_sub = (unsigned)ERRNO; mg_snprintf(NULL, NULL, /* No truncation check for ebuf */ error->text, error->text_buffer_size, "connect(%s:%d): error %s", host, port, strerror(sockerr)); } closesocket(*sock); *sock = INVALID_SOCKET; return 0; } return 1; } CIVETWEB_API int mg_url_encode(const char *src, char *dst, size_t dst_len) { static const char *dont_escape = "._-$,;~()"; static const char *hex = "0123456789abcdef"; char *pos = dst; const char *end = dst + dst_len - 1; for (; ((*src != '\0') && (pos < end)); src++, pos++) { if (isalnum((unsigned char)*src) || (strchr(dont_escape, *src) != NULL)) { *pos = *src; } else if (pos + 2 < end) { pos[0] = '%'; pos[1] = hex[(unsigned char)*src >> 4]; pos[2] = hex[(unsigned char)*src & 0xf]; pos += 2; } else { break; } } *pos = '\0'; return (*src == '\0') ? (int)(pos - dst) : -1; } /* Return 0 on success, non-zero if an error occurs. */ static int print_dir_entry(struct mg_connection *conn, struct de *de) { size_t namesize, escsize, i; char *href, *esc, *p; char size[64], mod[64]; #if defined(REENTRANT_TIME) struct tm _tm; struct tm *tm = &_tm; #else struct tm *tm; #endif /* Estimate worst case size for encoding and escaping */ namesize = strlen(de->file_name) + 1; escsize = de->file_name[strcspn(de->file_name, "&<>")] ? namesize * 5 : 0; href = (char *)mg_malloc(namesize * 3 + escsize); if (href == NULL) { return -1; } mg_url_encode(de->file_name, href, namesize * 3); esc = NULL; if (escsize > 0) { /* HTML escaping needed */ esc = href + namesize * 3; for (i = 0, p = esc; de->file_name[i]; i++, p += strlen(p)) { mg_strlcpy(p, de->file_name + i, 2); if (*p == '&') { strcpy(p, "&"); } else if (*p == '<') { strcpy(p, "<"); } else if (*p == '>') { strcpy(p, ">"); } } } if (de->file.is_directory) { mg_snprintf(conn, NULL, /* Buffer is big enough */ size, sizeof(size), "%s", "[DIRECTORY]"); } else { /* We use (signed) cast below because MSVC 6 compiler cannot * convert unsigned __int64 to double. Sigh. */ if (de->file.size < 1024) { mg_snprintf(conn, NULL, /* Buffer is big enough */ size, sizeof(size), "%d", (int)de->file.size); } else if (de->file.size < 0x100000) { mg_snprintf(conn, NULL, /* Buffer is big enough */ size, sizeof(size), "%.1fk", (double)de->file.size / 1024.0); } else if (de->file.size < 0x40000000) { mg_snprintf(conn, NULL, /* Buffer is big enough */ size, sizeof(size), "%.1fM", (double)de->file.size / 1048576); } else { mg_snprintf(conn, NULL, /* Buffer is big enough */ size, sizeof(size), "%.1fG", (double)de->file.size / 1073741824); } } /* Note: mg_snprintf will not cause a buffer overflow above. * So, string truncation checks are not required here. */ #if defined(REENTRANT_TIME) localtime_r(&de->file.last_modified, tm); #else tm = localtime(&de->file.last_modified); #endif if (tm != NULL) { strftime(mod, sizeof(mod), "%d-%b-%Y %H:%M", tm); } else { mg_strlcpy(mod, "01-Jan-1970 00:00", sizeof(mod)); } mg_printf(conn, "%s%s" " %s  %s\n", href, de->file.is_directory ? "/" : "", esc ? esc : de->file_name, de->file.is_directory ? "/" : "", mod, size); mg_free(href); return 0; } /* This function is called from send_directory() and used for * sorting directory entries by size, name, or modification time. */ static int compare_dir_entries(const void *p1, const void *p2, void *arg) { const char *query_string = (const char *)(arg != NULL ? arg : ""); if (p1 && p2) { const struct de *a = (const struct de *)p1, *b = (const struct de *)p2; int cmp_result = 0; if ((query_string == NULL) || (query_string[0] == '\0')) { query_string = "n"; } /* Sort Directories vs Files */ if (a->file.is_directory && !b->file.is_directory) { return -1; /* Always put directories on top */ } else if (!a->file.is_directory && b->file.is_directory) { return 1; /* Always put directories on top */ } /* Sort by size or date */ if (*query_string == 's') { cmp_result = (a->file.size == b->file.size) ? 0 : ((a->file.size > b->file.size) ? 1 : -1); } else if (*query_string == 'd') { cmp_result = (a->file.last_modified == b->file.last_modified) ? 0 : ((a->file.last_modified > b->file.last_modified) ? 1 : -1); } /* Sort by name: * if (*query_string == 'n') ... * but also sort files of same size/date by name as secondary criterion. */ if (cmp_result == 0) { cmp_result = strcmp(a->file_name, b->file_name); } /* For descending order, invert result */ return (query_string[1] == 'd') ? -cmp_result : cmp_result; } return 0; } static int must_hide_file(struct mg_connection *conn, const char *path) { if (conn && conn->dom_ctx) { const char *pw_pattern = "**" PASSWORDS_FILE_NAME "$"; const char *pattern = conn->dom_ctx->config[HIDE_FILES]; return (match_prefix_strlen(pw_pattern, path) > 0) || (match_prefix_strlen(pattern, path) > 0); } return 0; } #if !defined(NO_FILESYSTEMS) static int scan_directory(struct mg_connection *conn, const char *dir, void *data, int (*cb)(struct de *, void *)) { char path[UTF8_PATH_MAX]; struct dirent *dp; DIR *dirp; struct de de; int truncated; if ((dirp = mg_opendir(conn, dir)) == NULL) { return 0; } else { while ((dp = mg_readdir(dirp)) != NULL) { /* Do not show current dir and hidden files */ if (!strcmp(dp->d_name, ".") || !strcmp(dp->d_name, "..") || must_hide_file(conn, dp->d_name)) { continue; } mg_snprintf( conn, &truncated, path, sizeof(path), "%s/%s", dir, dp->d_name); /* If we don't memset stat structure to zero, mtime will have * garbage and strftime() will segfault later on in * print_dir_entry(). memset is required only if mg_stat() * fails. For more details, see * http://code.google.com/p/mongoose/issues/detail?id=79 */ memset(&de.file, 0, sizeof(de.file)); if (truncated) { /* If the path is not complete, skip processing. */ continue; } if (!mg_stat(conn, path, &de.file)) { mg_cry_internal(conn, "%s: mg_stat(%s) failed: %s", __func__, path, strerror(ERRNO)); } de.file_name = dp->d_name; if (cb(&de, data)) { /* stopped */ break; } } (void)mg_closedir(dirp); } return 1; } #endif /* NO_FILESYSTEMS */ #if !defined(NO_FILES) static int remove_directory(struct mg_connection *conn, const char *dir) { char path[UTF8_PATH_MAX]; struct dirent *dp; DIR *dirp; struct de de; int truncated; int ok = 1; if ((dirp = mg_opendir(conn, dir)) == NULL) { return 0; } else { while ((dp = mg_readdir(dirp)) != NULL) { /* Do not show current dir (but show hidden files as they will * also be removed) */ if (!strcmp(dp->d_name, ".") || !strcmp(dp->d_name, "..")) { continue; } mg_snprintf( conn, &truncated, path, sizeof(path), "%s/%s", dir, dp->d_name); /* If we don't memset stat structure to zero, mtime will have * garbage and strftime() will segfault later on in * print_dir_entry(). memset is required only if mg_stat() * fails. For more details, see * http://code.google.com/p/mongoose/issues/detail?id=79 */ memset(&de.file, 0, sizeof(de.file)); if (truncated) { /* Do not delete anything shorter */ ok = 0; continue; } if (!mg_stat(conn, path, &de.file)) { mg_cry_internal(conn, "%s: mg_stat(%s) failed: %s", __func__, path, strerror(ERRNO)); ok = 0; } if (de.file.is_directory) { if (remove_directory(conn, path) == 0) { ok = 0; } } else { /* This will fail file is the file is in memory */ if (mg_remove(conn, path) == 0) { ok = 0; } } } (void)mg_closedir(dirp); IGNORE_UNUSED_RESULT(rmdir(dir)); } return ok; } #endif struct dir_scan_data { struct de *entries; size_t num_entries; size_t arr_size; }; #if !defined(NO_FILESYSTEMS) static int dir_scan_callback(struct de *de, void *data) { struct dir_scan_data *dsd = (struct dir_scan_data *)data; struct de *entries = dsd->entries; if ((entries == NULL) || (dsd->num_entries >= dsd->arr_size)) { /* Here "entries" is a temporary pointer and can be replaced, * "dsd->entries" is the original pointer */ entries = (struct de *)mg_realloc(entries, dsd->arr_size * 2 * sizeof(entries[0])); if (entries == NULL) { /* stop scan */ return 1; } dsd->entries = entries; dsd->arr_size *= 2; } entries[dsd->num_entries].file_name = mg_strdup(de->file_name); if (entries[dsd->num_entries].file_name == NULL) { /* stop scan */ return 1; } entries[dsd->num_entries].file = de->file; dsd->num_entries++; return 0; } static void handle_directory_request(struct mg_connection *conn, const char *dir) { size_t i; int sort_direction; struct dir_scan_data data = {NULL, 0, 128}; char date[64], *esc, *p; const char *title; time_t curtime = time(NULL); if (!conn) { return; } if (!scan_directory(conn, dir, &data, dir_scan_callback)) { mg_send_http_error(conn, 500, "Error: Cannot open directory\nopendir(%s): %s", dir, strerror(ERRNO)); return; } gmt_time_string(date, sizeof(date), &curtime); esc = NULL; title = conn->request_info.local_uri; if (title[strcspn(title, "&<>")]) { /* HTML escaping needed */ esc = (char *)mg_malloc(strlen(title) * 5 + 1); if (esc) { for (i = 0, p = esc; title[i]; i++, p += strlen(p)) { mg_strlcpy(p, title + i, 2); if (*p == '&') { strcpy(p, "&"); } else if (*p == '<') { strcpy(p, "<"); } else if (*p == '>') { strcpy(p, ">"); } } } else { title = ""; } } sort_direction = ((conn->request_info.query_string != NULL) && (conn->request_info.query_string[0] != '\0') && (conn->request_info.query_string[1] == 'd')) ? 'a' : 'd'; conn->must_close = 1; /* Create 200 OK response */ mg_response_header_start(conn, 200); send_static_cache_header(conn); send_additional_header(conn); mg_response_header_add(conn, "Content-Type", "text/html; charset=utf-8", -1); /* Send all headers */ mg_response_header_send(conn); /* Body */ mg_printf(conn, "" "Index of %s" "" "

Index of %s

"
	          ""
	          ""
	          ""
	          "",
	          esc ? esc : title,
	          esc ? esc : title,
	          sort_direction,
	          sort_direction,
	          sort_direction);
	mg_free(esc);

	/* Print first entry - link to a parent directory */
	mg_printf(conn,
	          ""
	          "\n",
	          "..",
	          "Parent directory",
	          "-",
	          "-");

	/* Sort and print directory entries */
	if (data.entries != NULL) {
		mg_sort(data.entries,
		        data.num_entries,
		        sizeof(data.entries[0]),
		        compare_dir_entries,
		        (void *)conn->request_info.query_string);
		for (i = 0; i < data.num_entries; i++) {
			print_dir_entry(conn, &data.entries[i]);
			mg_free(data.entries[i].file_name);
		}
		mg_free(data.entries);
	}

	mg_printf(conn, "%s", "
NameModifiedSize

%s %s  %s
"); conn->status_code = 200; } #endif /* NO_FILESYSTEMS */ /* Send len bytes from the opened file to the client. */ static void send_file_data(struct mg_connection *conn, struct mg_file *filep, int64_t offset, int64_t len, int no_buffering) { char buf[MG_BUF_LEN]; int to_read, num_read, num_written; int64_t size; if (!filep || !conn) { return; } /* Sanity check the offset */ size = (filep->stat.size > INT64_MAX) ? INT64_MAX : (int64_t)(filep->stat.size); offset = (offset < 0) ? 0 : ((offset > size) ? size : offset); if (len > 0 && filep->access.fp != NULL) { /* file stored on disk */ #if defined(__linux__) /* sendfile is only available for Linux */ if ((conn->ssl == 0) && (conn->throttle == 0) && (!mg_strcasecmp(conn->dom_ctx->config[ALLOW_SENDFILE_CALL], "yes"))) { off_t sf_offs = (off_t)offset; ssize_t sf_sent; int sf_file = fileno(filep->access.fp); int loop_cnt = 0; do { /* 2147479552 (0x7FFFF000) is a limit found by experiment on * 64 bit Linux (2^31 minus one memory page of 4k?). */ size_t sf_tosend = (size_t)((len < 0x7FFFF000) ? len : 0x7FFFF000); sf_sent = sendfile(conn->client.sock, sf_file, &sf_offs, sf_tosend); if (sf_sent > 0) { len -= sf_sent; offset += sf_sent; } else if (loop_cnt == 0) { /* This file can not be sent using sendfile. * This might be the case for pseudo-files in the * /sys/ and /proc/ file system. * Use the regular user mode copy code instead. */ break; } else if (sf_sent == 0) { /* No error, but 0 bytes sent. May be EOF? */ return; } loop_cnt++; } while ((len > 0) && (sf_sent >= 0)); if (sf_sent > 0) { return; /* OK */ } /* sf_sent<0 means error, thus fall back to the classic way */ /* This is always the case, if sf_file is not a "normal" file, * e.g., for sending data from the output of a CGI process. */ offset = (int64_t)sf_offs; } #endif if ((offset > 0) && (fseeko(filep->access.fp, offset, SEEK_SET) != 0)) { mg_cry_internal(conn, "%s: fseeko() failed: %s", __func__, strerror(ERRNO)); mg_send_http_error( conn, 500, "%s", "Error: Unable to access file at requested position."); } else { while (len > 0) { /* Calculate how much to read from the file into the buffer. */ /* If no_buffering is set, we should not wait until the * CGI->Server buffer is filled, but send everything * immediately. In theory buffering could be turned off using * setbuf(filep->access.fp, NULL); * setvbuf(filep->access.fp, NULL, _IONBF, 0); * but in practice this does not work. A "Linux only" solution * may be to use select(). The only portable way is to read byte * by byte, but this is quite inefficient from a performance * point of view. */ to_read = no_buffering ? 1 : sizeof(buf); if ((int64_t)to_read > len) { to_read = (int)len; } /* Read from file, exit the loop on error */ if ((num_read = (int)fread(buf, 1, (size_t)to_read, filep->access.fp)) <= 0) { break; } /* Send read bytes to the client, exit the loop on error */ if ((num_written = mg_write(conn, buf, (size_t)num_read)) != num_read) { break; } /* Both read and were successful, adjust counters */ len -= num_written; } } } } static int parse_range_header(const char *header, int64_t *a, int64_t *b) { return sscanf(header, "bytes=%" INT64_FMT "-%" INT64_FMT, a, b); // NOLINT(cert-err34-c) 'sscanf' used to convert a string // to an integer value, but function will not report // conversion errors; consider using 'strtol' instead } static void construct_etag(char *buf, size_t buf_len, const struct mg_file_stat *filestat) { if ((filestat != NULL) && (buf != NULL)) { mg_snprintf(NULL, NULL, /* All calls to construct_etag use 64 byte buffer */ buf, buf_len, "\"%lx.%" INT64_FMT "\"", (unsigned long)filestat->last_modified, filestat->size); } } static void fclose_on_exec(struct mg_file_access *filep, struct mg_connection *conn) { if (filep != NULL && filep->fp != NULL) { #if defined(_WIN32) (void)conn; /* Unused. */ #else if (fcntl(fileno(filep->fp), F_SETFD, FD_CLOEXEC) != 0) { mg_cry_internal(conn, "%s: fcntl(F_SETFD FD_CLOEXEC) failed: %s", __func__, strerror(ERRNO)); } #endif } } #if defined(USE_ZLIB) #include "mod_zlib.h" #endif #if !defined(NO_FILESYSTEMS) static void handle_static_file_request(struct mg_connection *conn, const char *path, struct mg_file *filep, const char *mime_type, const char *additional_headers) { char lm[64], etag[64]; char range[128]; /* large enough, so there will be no overflow */ const char *range_hdr; int64_t cl, r1, r2; struct vec mime_vec; int n, truncated; char gz_path[UTF8_PATH_MAX]; const char *encoding = 0; int is_head_request; #if defined(USE_ZLIB) /* Compression is allowed, unless there is a reason not to use * compression. If the file is already compressed, too small or a * "range" request was made, on the fly compression is not possible. */ int allow_on_the_fly_compression = 1; #endif if ((conn == NULL) || (conn->dom_ctx == NULL) || (filep == NULL)) { return; } is_head_request = !strcmp(conn->request_info.request_method, "HEAD"); if (mime_type == NULL) { get_mime_type(conn, path, &mime_vec); } else { mime_vec.ptr = mime_type; mime_vec.len = strlen(mime_type); } if (filep->stat.size > INT64_MAX) { mg_send_http_error(conn, 500, "Error: File size is too large to send\n%" INT64_FMT, filep->stat.size); return; } cl = (int64_t)filep->stat.size; conn->status_code = 200; range[0] = '\0'; #if defined(USE_ZLIB) /* if this file is in fact a pre-gzipped file, rewrite its filename * it's important to rewrite the filename after resolving * the mime type from it, to preserve the actual file's type */ if (!conn->accept_gzip) { allow_on_the_fly_compression = 0; } #endif /* Check if there is a range header */ range_hdr = mg_get_header(conn, "Range"); /* For gzipped files, add *.gz */ if (filep->stat.is_gzipped) { mg_snprintf(conn, &truncated, gz_path, sizeof(gz_path), "%s.gz", path); if (truncated) { mg_send_http_error(conn, 500, "Error: Path of zipped file too long (%s)", path); return; } path = gz_path; encoding = "gzip"; #if defined(USE_ZLIB) /* File is already compressed. No "on the fly" compression. */ allow_on_the_fly_compression = 0; #endif } else if ((conn->accept_gzip) && (range_hdr == NULL) && (filep->stat.size >= MG_FILE_COMPRESSION_SIZE_LIMIT)) { struct mg_file_stat file_stat; mg_snprintf(conn, &truncated, gz_path, sizeof(gz_path), "%s.gz", path); if (!truncated && mg_stat(conn, gz_path, &file_stat) && !file_stat.is_directory) { file_stat.is_gzipped = 1; filep->stat = file_stat; cl = (int64_t)filep->stat.size; path = gz_path; encoding = "gzip"; #if defined(USE_ZLIB) /* File is already compressed. No "on the fly" compression. */ allow_on_the_fly_compression = 0; #endif } } if (!mg_fopen(conn, path, MG_FOPEN_MODE_READ, filep)) { mg_send_http_error(conn, 500, "Error: Cannot open file\nfopen(%s): %s", path, strerror(ERRNO)); return; } fclose_on_exec(&filep->access, conn); /* If "Range" request was made: parse header, send only selected part * of the file. */ r1 = r2 = 0; if ((range_hdr != NULL) && ((n = parse_range_header(range_hdr, &r1, &r2)) > 0) && (r1 >= 0) && (r2 >= 0)) { /* actually, range requests don't play well with a pre-gzipped * file (since the range is specified in the uncompressed space) */ if (filep->stat.is_gzipped) { mg_send_http_error( conn, 416, /* 416 = Range Not Satisfiable */ "%s", "Error: Range requests in gzipped files are not supported"); (void)mg_fclose( &filep->access); /* ignore error on read only file */ return; } conn->status_code = 206; cl = (n == 2) ? (((r2 > cl) ? cl : r2) - r1 + 1) : (cl - r1); mg_snprintf(conn, NULL, /* range buffer is big enough */ range, sizeof(range), "bytes " "%" INT64_FMT "-%" INT64_FMT "/%" INT64_FMT, r1, r1 + cl - 1, filep->stat.size); #if defined(USE_ZLIB) /* Do not compress ranges. */ allow_on_the_fly_compression = 0; #endif } /* Do not compress small files. Small files do not benefit from file * compression, but there is still some overhead. */ #if defined(USE_ZLIB) if (filep->stat.size < MG_FILE_COMPRESSION_SIZE_LIMIT) { /* File is below the size limit. */ allow_on_the_fly_compression = 0; } #endif /* Prepare Etag, and Last-Modified headers. */ gmt_time_string(lm, sizeof(lm), &filep->stat.last_modified); construct_etag(etag, sizeof(etag), &filep->stat); /* Create 2xx (200, 206) response */ mg_response_header_start(conn, conn->status_code); send_static_cache_header(conn); send_additional_header(conn); send_cors_header(conn); mg_response_header_add(conn, "Content-Type", mime_vec.ptr, (int)mime_vec.len); mg_response_header_add(conn, "Last-Modified", lm, -1); mg_response_header_add(conn, "Etag", etag, -1); #if defined(USE_ZLIB) /* On the fly compression allowed */ if (allow_on_the_fly_compression) { /* For on the fly compression, we don't know the content size in * advance, so we have to use chunked encoding */ encoding = "gzip"; if (conn->protocol_type == PROTOCOL_TYPE_HTTP1) { /* HTTP/2 is always using "chunks" (frames) */ mg_response_header_add(conn, "Transfer-Encoding", "chunked", -1); } } else #endif { /* Without on-the-fly compression, we know the content-length * and we can use ranges (with on-the-fly compression we cannot). * So we send these response headers only in this case. */ char len[32]; int trunc = 0; mg_snprintf(conn, &trunc, len, sizeof(len), "%" INT64_FMT, cl); if (!trunc) { mg_response_header_add(conn, "Content-Length", len, -1); } mg_response_header_add(conn, "Accept-Ranges", "bytes", -1); } if (encoding) { mg_response_header_add(conn, "Content-Encoding", encoding, -1); } if (range[0] != 0) { mg_response_header_add(conn, "Content-Range", range, -1); } /* The code above does not add any header starting with X- to make * sure no one of the additional_headers is included twice */ if ((additional_headers != NULL) && (*additional_headers != 0)) { mg_response_header_add_lines(conn, additional_headers); } /* Send all headers */ mg_response_header_send(conn); if (!is_head_request) { #if defined(USE_ZLIB) if (allow_on_the_fly_compression) { /* Compress and send */ send_compressed_data(conn, filep); } else #endif { /* Send file directly */ send_file_data(conn, filep, r1, cl, 0); /* send static file */ } } (void)mg_fclose(&filep->access); /* ignore error on read only file */ } CIVETWEB_API int mg_send_file_body(struct mg_connection *conn, const char *path) { struct mg_file file = STRUCT_FILE_INITIALIZER; if (!mg_fopen(conn, path, MG_FOPEN_MODE_READ, &file)) { return -1; } fclose_on_exec(&file.access, conn); send_file_data(conn, &file, 0, INT64_MAX, 0); /* send static file */ (void)mg_fclose(&file.access); /* Ignore errors for readonly files */ return 0; /* >= 0 for OK */ } #endif /* NO_FILESYSTEMS */ #if !defined(NO_CACHING) /* Return True if we should reply 304 Not Modified. */ static int is_not_modified(const struct mg_connection *conn, const struct mg_file_stat *filestat) { char etag[64]; const char *ims = mg_get_header(conn, "If-Modified-Since"); const char *inm = mg_get_header(conn, "If-None-Match"); construct_etag(etag, sizeof(etag), filestat); return ((inm != NULL) && !mg_strcasecmp(etag, inm)) || ((ims != NULL) && (filestat->last_modified <= parse_date_string(ims))); } static void handle_not_modified_static_file_request(struct mg_connection *conn, struct mg_file *filep) { char lm[64], etag[64]; if ((conn == NULL) || (filep == NULL)) { return; } gmt_time_string(lm, sizeof(lm), &filep->stat.last_modified); construct_etag(etag, sizeof(etag), &filep->stat); /* Create 304 "not modified" response */ mg_response_header_start(conn, 304); send_static_cache_header(conn); send_additional_header(conn); mg_response_header_add(conn, "Last-Modified", lm, -1); mg_response_header_add(conn, "Etag", etag, -1); /* Send all headers */ mg_response_header_send(conn); } #endif #if !defined(NO_FILESYSTEMS) CIVETWEB_API void mg_send_file(struct mg_connection *conn, const char *path) { mg_send_mime_file2(conn, path, NULL, NULL); } CIVETWEB_API void mg_send_mime_file(struct mg_connection *conn, const char *path, const char *mime_type) { mg_send_mime_file2(conn, path, mime_type, NULL); } CIVETWEB_API void mg_send_mime_file2(struct mg_connection *conn, const char *path, const char *mime_type, const char *additional_headers) { struct mg_file file = STRUCT_FILE_INITIALIZER; if (!conn) { /* No conn */ return; } if (mg_stat(conn, path, &file.stat)) { #if !defined(NO_CACHING) if (is_not_modified(conn, &file.stat)) { /* Send 304 "Not Modified" - this must not send any body data */ handle_not_modified_static_file_request(conn, &file); } else #endif /* NO_CACHING */ if (file.stat.is_directory) { if (!mg_strcasecmp(conn->dom_ctx->config[ENABLE_DIRECTORY_LISTING], "yes")) { handle_directory_request(conn, path); } else { mg_send_http_error(conn, 403, "%s", "Error: Directory listing denied"); } } else { handle_static_file_request( conn, path, &file, mime_type, additional_headers); } } else { mg_send_http_error(conn, 404, "%s", "Error: File not found"); } } /* For a given PUT path, create all intermediate subdirectories. * Return 0 if the path itself is a directory. * Return 1 if the path leads to a file. * Return -1 for if the path is too long. * Return -2 if path can not be created. */ static int put_dir(struct mg_connection *conn, const char *path) { char buf[UTF8_PATH_MAX]; const char *s, *p; struct mg_file file = STRUCT_FILE_INITIALIZER; size_t len; int res = 1; for (s = p = path + 2; (p = strchr(s, '/')) != NULL; s = ++p) { len = (size_t)(p - path); if (len >= sizeof(buf)) { /* path too long */ res = -1; break; } memcpy(buf, path, len); buf[len] = '\0'; /* Try to create intermediate directory */ DEBUG_TRACE("mkdir(%s)", buf); if (!mg_stat(conn, buf, &file.stat) && mg_mkdir(conn, buf, 0755) != 0) { /* path does not exist and can not be created */ res = -2; break; } /* Is path itself a directory? */ if (p[1] == '\0') { res = 0; } } return res; } static void remove_bad_file(const struct mg_connection *conn, const char *path) { int r = mg_remove(conn, path); if (r != 0) { mg_cry_internal(conn, "%s: Cannot remove invalid file %s", __func__, path); } } CIVETWEB_API long long mg_store_body(struct mg_connection *conn, const char *path) { char buf[MG_BUF_LEN]; long long len = 0; int ret, n; struct mg_file fi; if (conn->consumed_content != 0) { mg_cry_internal(conn, "%s: Contents already consumed", __func__); return -11; } ret = put_dir(conn, path); if (ret < 0) { /* -1 for path too long, * -2 for path can not be created. */ return ret; } if (ret != 1) { /* Return 0 means, path itself is a directory. */ return 0; } if (mg_fopen(conn, path, MG_FOPEN_MODE_WRITE, &fi) == 0) { return -12; } ret = mg_read(conn, buf, sizeof(buf)); while (ret > 0) { n = (int)fwrite(buf, 1, (size_t)ret, fi.access.fp); if (n != ret) { (void)mg_fclose( &fi.access); /* File is bad and will be removed anyway. */ remove_bad_file(conn, path); return -13; } len += ret; ret = mg_read(conn, buf, sizeof(buf)); } /* File is open for writing. If fclose fails, there was probably an * error flushing the buffer to disk, so the file on disk might be * broken. Delete it and return an error to the caller. */ if (mg_fclose(&fi.access) != 0) { remove_bad_file(conn, path); return -14; } return len; } #endif /* NO_FILESYSTEMS */ /* Parse a buffer: * Forward the string pointer till the end of a word, then * terminate it and forward till the begin of the next word. */ static int skip_to_end_of_word_and_terminate(char **ppw, int eol) { /* Forward until a space is found - use isgraph here */ /* See http://www.cplusplus.com/reference/cctype/ */ while (isgraph((unsigned char)**ppw)) { (*ppw)++; } /* Check end of word */ if (eol) { /* must be a end of line */ if ((**ppw != '\r') && (**ppw != '\n')) { return -1; } } else { /* must be a end of a word, but not a line */ if (**ppw != ' ') { return -1; } } /* Terminate and forward to the next word */ do { **ppw = 0; (*ppw)++; } while (isspace((unsigned char)**ppw)); /* Check after term */ if (!eol) { /* if it's not the end of line, there must be a next word */ if (!isgraph((unsigned char)**ppw)) { return -1; } } /* ok */ return 1; } /* Parse HTTP headers from the given buffer, advance buf pointer * to the point where parsing stopped. * All parameters must be valid pointers (not NULL). * Return <0 on error. */ static int parse_http_headers(char **buf, struct mg_header hdr[MG_MAX_HEADERS]) { int i; int num_headers = 0; for (i = 0; i < (int)MG_MAX_HEADERS; i++) { char *dp = *buf; /* Skip all ASCII characters (>SPACE, <127), to find a ':' */ while ((*dp != ':') && (*dp >= 33) && (*dp <= 126)) { dp++; } if (dp == *buf) { /* End of headers reached. */ break; } /* Drop all spaces after header name before : */ while (*dp == ' ') { *dp = 0; dp++; } if (*dp != ':') { /* This is not a valid field. */ return -1; } /* End of header key (*dp == ':') */ /* Truncate here and set the key name */ *dp = 0; hdr[i].name = *buf; /* Skip all spaces */ do { dp++; } while ((*dp == ' ') || (*dp == '\t')); /* The rest of the line is the value */ hdr[i].value = dp; /* Find end of line */ while ((*dp != 0) && (*dp != '\r') && (*dp != '\n')) { dp++; }; /* eliminate \r */ if (*dp == '\r') { *dp = 0; dp++; if (*dp != '\n') { /* This is not a valid line. */ return -1; } } /* here *dp is either 0 or '\n' */ /* in any case, we have a new header */ num_headers = i + 1; if (*dp) { *dp = 0; dp++; *buf = dp; if ((dp[0] == '\r') || (dp[0] == '\n')) { /* This is the end of the header */ break; } } else { *buf = dp; break; } } return num_headers; } struct mg_http_method_info { const char *name; int request_has_body; int response_has_body; int is_safe; int is_idempotent; int is_cacheable; }; /* https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods */ static const struct mg_http_method_info http_methods[] = { /* HTTP (RFC 2616) */ {"GET", 0, 1, 1, 1, 1}, {"POST", 1, 1, 0, 0, 0}, {"PUT", 1, 0, 0, 1, 0}, {"DELETE", 0, 0, 0, 1, 0}, {"HEAD", 0, 0, 1, 1, 1}, {"OPTIONS", 0, 0, 1, 1, 0}, {"CONNECT", 1, 1, 0, 0, 0}, /* TRACE method (RFC 2616) is not supported for security reasons */ /* PATCH method (RFC 5789) */ {"PATCH", 1, 0, 0, 0, 0}, /* PATCH method only allowed for CGI/Lua/LSP and callbacks. */ /* WEBDAV (RFC 2518) */ {"PROPFIND", 0, 1, 1, 1, 0}, /* http://www.webdav.org/specs/rfc4918.html, 9.1: * Some PROPFIND results MAY be cached, with care, * as there is no cache validation mechanism for * most properties. This method is both safe and * idempotent (see Section 9.1 of [RFC2616]). */ {"MKCOL", 0, 0, 0, 1, 0}, /* http://www.webdav.org/specs/rfc4918.html, 9.1: * When MKCOL is invoked without a request body, * the newly created collection SHOULD have no * members. A MKCOL request message may contain * a message body. The precise behavior of a MKCOL * request when the body is present is undefined, * ... ==> We do not support MKCOL with body data. * This method is idempotent, but not safe (see * Section 9.1 of [RFC2616]). Responses to this * method MUST NOT be cached. */ /* Methods for write access to files on WEBDAV (RFC 2518) */ {"LOCK", 1, 1, 0, 0, 0}, {"UNLOCK", 1, 0, 0, 0, 0}, {"PROPPATCH", 1, 1, 0, 0, 0}, {"COPY", 1, 0, 0, 0, 0}, {"MOVE", 1, 1, 0, 0, 0}, /* Unsupported WEBDAV Methods: */ /* + 11 methods from RFC 3253 */ /* ORDERPATCH (RFC 3648) */ /* ACL (RFC 3744) */ /* SEARCH (RFC 5323) */ /* + MicroSoft extensions * https://msdn.microsoft.com/en-us/library/aa142917.aspx */ /* REPORT method (RFC 3253) */ {"REPORT", 1, 1, 1, 1, 1}, /* REPORT method only allowed for CGI/Lua/LSP and callbacks. */ /* It was defined for WEBDAV in RFC 3253, Sec. 3.6 * (https://tools.ietf.org/html/rfc3253#section-3.6), but seems * to be useful for REST in case a "GET request with body" is * required. */ {NULL, 0, 0, 0, 0, 0} /* end of list */ }; /* All method names */ static char *all_methods = NULL; /* Built by mg_init_library */ static const struct mg_http_method_info * get_http_method_info(const char *method) { /* Check if the method is known to the server. The list of all known * HTTP methods can be found here at * http://www.iana.org/assignments/http-methods/http-methods.xhtml */ const struct mg_http_method_info *m = http_methods; while (m->name) { if (!strcmp(m->name, method)) { return m; } m++; } return NULL; } static int is_valid_http_method(const char *method) { return (get_http_method_info(method) != NULL); } /* Parse HTTP request, fill in mg_request_info structure. * This function modifies the buffer by NUL-terminating * HTTP request components, header names and header values. * Parameters: * buf (in/out): pointer to the HTTP header to parse and split * len (in): length of HTTP header buffer * re (out): parsed header as mg_request_info * buf and ri must be valid pointers (not NULL), len>0. * Returns <0 on error. */ static int parse_http_request(char *buf, int len, struct mg_request_info *ri) { int request_length; int init_skip = 0; /* Reset attributes. DO NOT TOUCH is_ssl, remote_addr, * remote_port */ ri->remote_user = ri->request_method = ri->request_uri = ri->http_version = NULL; ri->num_headers = 0; /* RFC says that all initial whitespaces should be ignored */ /* This included all leading \r and \n (isspace) */ /* See table: http://www.cplusplus.com/reference/cctype/ */ while ((len > 0) && isspace((unsigned char)*buf)) { buf++; len--; init_skip++; } if (len == 0) { /* Incomplete request */ return 0; } /* Control characters are not allowed, including zero */ if (iscntrl((unsigned char)*buf)) { return -1; } /* Find end of HTTP header */ request_length = get_http_header_len(buf, len); if (request_length <= 0) { return request_length; } buf[request_length - 1] = '\0'; if ((*buf == 0) || (*buf == '\r') || (*buf == '\n')) { return -1; } /* The first word has to be the HTTP method */ ri->request_method = buf; if (skip_to_end_of_word_and_terminate(&buf, 0) <= 0) { return -1; } /* The second word is the URI */ ri->request_uri = buf; if (skip_to_end_of_word_and_terminate(&buf, 0) <= 0) { return -1; } /* Next would be the HTTP version */ ri->http_version = buf; if (skip_to_end_of_word_and_terminate(&buf, 1) <= 0) { return -1; } /* Check for a valid HTTP version key */ if (strncmp(ri->http_version, "HTTP/", 5) != 0) { /* Invalid request */ return -1; } ri->http_version += 5; /* Check for a valid http method */ if (!is_valid_http_method(ri->request_method)) { return -1; } /* Parse all HTTP headers */ ri->num_headers = parse_http_headers(&buf, ri->http_headers); if (ri->num_headers < 0) { /* Error while parsing headers */ return -1; } return request_length + init_skip; } static int parse_http_response(char *buf, int len, struct mg_response_info *ri) { int response_length; int init_skip = 0; char *tmp, *tmp2; long l; /* Initialize elements. */ ri->http_version = ri->status_text = NULL; ri->num_headers = ri->status_code = 0; /* RFC says that all initial whitespaces should be ignored */ /* This included all leading \r and \n (isspace) */ /* See table: http://www.cplusplus.com/reference/cctype/ */ while ((len > 0) && isspace((unsigned char)*buf)) { buf++; len--; init_skip++; } if (len == 0) { /* Incomplete request */ return 0; } /* Control characters are not allowed, including zero */ if (iscntrl((unsigned char)*buf)) { return -1; } /* Find end of HTTP header */ response_length = get_http_header_len(buf, len); if (response_length <= 0) { return response_length; } buf[response_length - 1] = '\0'; if ((*buf == 0) || (*buf == '\r') || (*buf == '\n')) { return -1; } /* The first word is the HTTP version */ /* Check for a valid HTTP version key */ if (strncmp(buf, "HTTP/", 5) != 0) { /* Invalid request */ return -1; } buf += 5; if (!isgraph((unsigned char)buf[0])) { /* Invalid request */ return -1; } ri->http_version = buf; if (skip_to_end_of_word_and_terminate(&buf, 0) <= 0) { return -1; } /* The second word is the status as a number */ tmp = buf; if (skip_to_end_of_word_and_terminate(&buf, 0) <= 0) { return -1; } l = strtol(tmp, &tmp2, 10); if ((l < 100) || (l >= 1000) || ((tmp2 - tmp) != 3) || (*tmp2 != 0)) { /* Everything else but a 3 digit code is invalid */ return -1; } ri->status_code = (int)l; /* The rest of the line is the status text */ ri->status_text = buf; /* Find end of status text */ /* isgraph or isspace = isprint */ while (isprint((unsigned char)*buf)) { buf++; } if ((*buf != '\r') && (*buf != '\n')) { return -1; } /* Terminate string and forward buf to next line */ do { *buf = 0; buf++; } while (isspace((unsigned char)*buf)); /* Parse all HTTP headers */ ri->num_headers = parse_http_headers(&buf, ri->http_headers); if (ri->num_headers < 0) { /* Error while parsing headers */ return -1; } return response_length + init_skip; } /* Keep reading the input (either opened file descriptor fd, or socket sock, * or SSL descriptor ssl) into buffer buf, until \r\n\r\n appears in the * buffer (which marks the end of HTTP request). Buffer buf may already * have some data. The length of the data is stored in nread. * Upon every read operation, increase nread by the number of bytes read. */ static int read_message(FILE *fp, struct mg_connection *conn, char *buf, int bufsiz, int *nread) { int request_len, n = 0; struct timespec last_action_time; double request_timeout; if (!conn) { return 0; } memset(&last_action_time, 0, sizeof(last_action_time)); if (conn->dom_ctx->config[REQUEST_TIMEOUT]) { /* value of request_timeout is in seconds, config in milliseconds */ request_timeout = strtod(conn->dom_ctx->config[REQUEST_TIMEOUT], NULL) / 1000.0; } else { request_timeout = strtod(config_options[REQUEST_TIMEOUT].default_value, NULL) / 1000.0; } if (conn->handled_requests > 0) { if (conn->dom_ctx->config[KEEP_ALIVE_TIMEOUT]) { request_timeout = strtod(conn->dom_ctx->config[KEEP_ALIVE_TIMEOUT], NULL) / 1000.0; } } request_len = get_http_header_len(buf, *nread); while (request_len == 0) { /* Full request not yet received */ if (!STOP_FLAG_IS_ZERO(&conn->phys_ctx->stop_flag)) { /* Server is to be stopped. */ return -1; } if (*nread >= bufsiz) { /* Request too long */ return -2; } n = pull_inner( fp, conn, buf + *nread, bufsiz - *nread, request_timeout); if (n == -2) { /* Receive error */ return -1; } /* update clock after every read request */ clock_gettime(CLOCK_MONOTONIC, &last_action_time); if (n > 0) { *nread += n; request_len = get_http_header_len(buf, *nread); } if ((request_len == 0) && (request_timeout >= 0)) { if (mg_difftimespec(&last_action_time, &(conn->req_time)) > request_timeout) { /* Timeout */ return -1; } } } return request_len; } #if !defined(NO_CGI) || !defined(NO_FILES) static int forward_body_data(struct mg_connection *conn, FILE *fp, SOCKET sock, SSL *ssl) { const char *expect; char buf[MG_BUF_LEN]; int success = 0; if (!conn) { return 0; } expect = mg_get_header(conn, "Expect"); DEBUG_ASSERT(fp != NULL); if (!fp) { mg_send_http_error(conn, 500, "%s", "Error: NULL File"); return 0; } if ((expect != NULL) && (mg_strcasecmp(expect, "100-continue") != 0)) { /* Client sent an "Expect: xyz" header and xyz is not 100-continue. */ mg_send_http_error(conn, 417, "Error: Can not fulfill expectation"); } else { if (expect != NULL) { (void)mg_printf(conn, "%s", "HTTP/1.1 100 Continue\r\n\r\n"); conn->status_code = 100; } else { conn->status_code = 200; } DEBUG_ASSERT(conn->consumed_content == 0); if (conn->consumed_content != 0) { mg_send_http_error(conn, 500, "%s", "Error: Size mismatch"); return 0; } for (;;) { int nread = mg_read(conn, buf, sizeof(buf)); if (nread <= 0) { success = (nread == 0); break; } if (push_all(conn->phys_ctx, fp, sock, ssl, buf, nread) != nread) { break; } } /* Each error code path in this function must send an error */ if (!success) { /* NOTE: Maybe some data has already been sent. */ /* TODO (low): If some data has been sent, a correct error * reply can no longer be sent, so just close the connection */ mg_send_http_error(conn, 500, "%s", ""); } } return success; } #endif #if defined(USE_TIMERS) #define TIMER_API static #include "timer.h" #endif /* USE_TIMERS */ #if !defined(NO_CGI) /* This structure helps to create an environment for the spawned CGI * program. * Environment is an array of "VARIABLE=VALUE\0" ASCII strings, * last element must be NULL. * However, on Windows there is a requirement that all these * VARIABLE=VALUE\0 * strings must reside in a contiguous buffer. The end of the buffer is * marked by two '\0' characters. * We satisfy both worlds: we create an envp array (which is vars), all * entries are actually pointers inside buf. */ struct cgi_environment { struct mg_connection *conn; /* Data block */ char *buf; /* Environment buffer */ size_t buflen; /* Space available in buf */ size_t bufused; /* Space taken in buf */ /* Index block */ char **var; /* char **envp */ size_t varlen; /* Number of variables available in var */ size_t varused; /* Number of variables stored in var */ }; static void addenv(struct cgi_environment *env, PRINTF_FORMAT_STRING(const char *fmt), ...) PRINTF_ARGS(2, 3); /* Append VARIABLE=VALUE\0 string to the buffer, and add a respective * pointer into the vars array. Assumes env != NULL and fmt != NULL. */ static void addenv(struct cgi_environment *env, const char *fmt, ...) { size_t i, n, space; int truncated = 0; char *added; va_list ap; if ((env->varlen - env->varused) < 2) { mg_cry_internal(env->conn, "%s: Cannot register CGI variable [%s]", __func__, fmt); return; } /* Calculate how much space is left in the buffer */ space = (env->buflen - env->bufused); do { /* Space for "\0\0" is always needed. */ if (space <= 2) { /* Allocate new buffer */ n = env->buflen + CGI_ENVIRONMENT_SIZE; added = (char *)mg_realloc_ctx(env->buf, n, env->conn->phys_ctx); if (!added) { /* Out of memory */ mg_cry_internal( env->conn, "%s: Cannot allocate memory for CGI variable [%s]", __func__, fmt); return; } /* Retarget pointers */ env->buf = added; env->buflen = n; for (i = 0, n = 0; i < env->varused; i++) { env->var[i] = added + n; n += strlen(added + n) + 1; } space = (env->buflen - env->bufused); } /* Make a pointer to the free space int the buffer */ added = env->buf + env->bufused; /* Copy VARIABLE=VALUE\0 string into the free space */ va_start(ap, fmt); mg_vsnprintf(env->conn, &truncated, added, space - 1, fmt, ap); va_end(ap); /* Do not add truncated strings to the environment */ if (truncated) { /* Reallocate the buffer */ space = 0; } } while (truncated); /* Calculate number of bytes added to the environment */ n = strlen(added) + 1; env->bufused += n; /* Append a pointer to the added string into the envp array */ env->var[env->varused] = added; env->varused++; } /* Return 0 on success, non-zero if an error occurs. */ static int prepare_cgi_environment(struct mg_connection *conn, const char *prog, struct cgi_environment *env, int cgi_config_idx) { const char *s; struct vec var_vec; char *p, src_addr[IP_ADDR_STR_LEN], http_var_name[128]; int i, truncated, uri_len; if ((conn == NULL) || (prog == NULL) || (env == NULL)) { return -1; } env->conn = conn; env->buflen = CGI_ENVIRONMENT_SIZE; env->bufused = 0; env->buf = (char *)mg_malloc_ctx(env->buflen, conn->phys_ctx); if (env->buf == NULL) { mg_cry_internal(conn, "%s: Not enough memory for environmental buffer", __func__); return -1; } env->varlen = MAX_CGI_ENVIR_VARS; env->varused = 0; env->var = (char **)mg_malloc_ctx(env->varlen * sizeof(char *), conn->phys_ctx); if (env->var == NULL) { mg_cry_internal(conn, "%s: Not enough memory for environmental variables", __func__); mg_free(env->buf); return -1; } addenv(env, "SERVER_NAME=%s", conn->dom_ctx->config[AUTHENTICATION_DOMAIN]); addenv(env, "SERVER_ROOT=%s", conn->dom_ctx->config[DOCUMENT_ROOT]); addenv(env, "DOCUMENT_ROOT=%s", conn->dom_ctx->config[DOCUMENT_ROOT]); addenv(env, "SERVER_SOFTWARE=CivetWeb/%s", mg_version()); /* Prepare the environment block */ addenv(env, "%s", "GATEWAY_INTERFACE=CGI/1.1"); addenv(env, "%s", "SERVER_PROTOCOL=HTTP/1.1"); addenv(env, "%s", "REDIRECT_STATUS=200"); /* For PHP */ addenv(env, "SERVER_PORT=%d", conn->request_info.server_port); sockaddr_to_string(src_addr, sizeof(src_addr), &conn->client.rsa); addenv(env, "REMOTE_ADDR=%s", src_addr); addenv(env, "REQUEST_METHOD=%s", conn->request_info.request_method); addenv(env, "REMOTE_PORT=%d", conn->request_info.remote_port); addenv(env, "REQUEST_URI=%s", conn->request_info.request_uri); addenv(env, "LOCAL_URI=%s", conn->request_info.local_uri); addenv(env, "LOCAL_URI_RAW=%s", conn->request_info.local_uri_raw); /* SCRIPT_NAME */ uri_len = (int)strlen(conn->request_info.local_uri); if (conn->path_info == NULL) { if (conn->request_info.local_uri[uri_len - 1] != '/') { /* URI: /path_to_script/script.cgi */ addenv(env, "SCRIPT_NAME=%s", conn->request_info.local_uri); } else { /* URI: /path_to_script/ ... using index.cgi */ const char *index_file = strrchr(prog, '/'); if (index_file) { addenv(env, "SCRIPT_NAME=%s%s", conn->request_info.local_uri, index_file + 1); } } } else { /* URI: /path_to_script/script.cgi/path_info */ addenv(env, "SCRIPT_NAME=%.*s", uri_len - (int)strlen(conn->path_info), conn->request_info.local_uri); } addenv(env, "SCRIPT_FILENAME=%s", prog); if (conn->path_info == NULL) { addenv(env, "PATH_TRANSLATED=%s", conn->dom_ctx->config[DOCUMENT_ROOT]); } else { addenv(env, "PATH_TRANSLATED=%s%s", conn->dom_ctx->config[DOCUMENT_ROOT], conn->path_info); } addenv(env, "HTTPS=%s", (conn->ssl == NULL) ? "off" : "on"); if ((s = mg_get_header(conn, "Content-Type")) != NULL) { addenv(env, "CONTENT_TYPE=%s", s); } if (conn->request_info.query_string != NULL) { addenv(env, "QUERY_STRING=%s", conn->request_info.query_string); } if ((s = mg_get_header(conn, "Content-Length")) != NULL) { addenv(env, "CONTENT_LENGTH=%s", s); } if ((s = getenv("PATH")) != NULL) { addenv(env, "PATH=%s", s); } if (conn->path_info != NULL) { addenv(env, "PATH_INFO=%s", conn->path_info); } if (conn->status_code > 0) { /* CGI error handler should show the status code */ addenv(env, "STATUS=%d", conn->status_code); } #if defined(_WIN32) if ((s = getenv("COMSPEC")) != NULL) { addenv(env, "COMSPEC=%s", s); } if ((s = getenv("SYSTEMROOT")) != NULL) { addenv(env, "SYSTEMROOT=%s", s); } if ((s = getenv("SystemDrive")) != NULL) { addenv(env, "SystemDrive=%s", s); } if ((s = getenv("ProgramFiles")) != NULL) { addenv(env, "ProgramFiles=%s", s); } if ((s = getenv("ProgramFiles(x86)")) != NULL) { addenv(env, "ProgramFiles(x86)=%s", s); } #else if ((s = getenv("LD_LIBRARY_PATH")) != NULL) { addenv(env, "LD_LIBRARY_PATH=%s", s); } #endif /* _WIN32 */ if ((s = getenv("PERLLIB")) != NULL) { addenv(env, "PERLLIB=%s", s); } if (conn->request_info.remote_user != NULL) { addenv(env, "REMOTE_USER=%s", conn->request_info.remote_user); addenv(env, "%s", "AUTH_TYPE=Digest"); } /* Add all headers as HTTP_* variables */ for (i = 0; i < conn->request_info.num_headers; i++) { (void)mg_snprintf(conn, &truncated, http_var_name, sizeof(http_var_name), "HTTP_%s", conn->request_info.http_headers[i].name); if (truncated) { mg_cry_internal(conn, "%s: HTTP header variable too long [%s]", __func__, conn->request_info.http_headers[i].name); continue; } /* Convert variable name into uppercase, and change - to _ */ for (p = http_var_name; *p != '\0'; p++) { if (*p == '-') { *p = '_'; } *p = (char)toupper((unsigned char)*p); } addenv(env, "%s=%s", http_var_name, conn->request_info.http_headers[i].value); } /* Add user-specified variables */ s = conn->dom_ctx->config[CGI_ENVIRONMENT + cgi_config_idx]; while ((s = next_option(s, &var_vec, NULL)) != NULL) { addenv(env, "%.*s", (int)var_vec.len, var_vec.ptr); } env->var[env->varused] = NULL; env->buf[env->bufused] = '\0'; return 0; } /* Data for CGI process control: PID and number of references */ struct process_control_data { pid_t pid; ptrdiff_t references; }; static int abort_cgi_process(void *data) { /* Waitpid checks for child status and won't work for a pid that does * not identify a child of the current process. Thus, if the pid is * reused, we will not affect a different process. */ struct process_control_data *proc = (struct process_control_data *)data; int status = 0; ptrdiff_t refs; pid_t ret_pid; ret_pid = waitpid(proc->pid, &status, WNOHANG); if ((ret_pid != (pid_t)-1) && (status == 0)) { /* Stop child process */ DEBUG_TRACE("CGI timer: Stop child process %d\n", proc->pid); kill(proc->pid, SIGABRT); /* Wait until process is terminated (don't leave zombies) */ while (waitpid(proc->pid, &status, 0) != (pid_t)-1) /* nop */ ; } else { DEBUG_TRACE("CGI timer: Child process %d already stopped\n", proc->pid); } /* Dec reference counter */ refs = mg_atomic_dec(&proc->references); if (refs == 0) { /* no more references - free data */ mg_free(data); } return 0; } /* Local (static) function assumes all arguments are valid. */ static void handle_cgi_request(struct mg_connection *conn, const char *prog, int cgi_config_idx) { char *buf; size_t buflen; int headers_len, data_len, i, truncated; int fdin[2] = {-1, -1}, fdout[2] = {-1, -1}, fderr[2] = {-1, -1}; const char *status, *status_text; char *pbuf, dir[UTF8_PATH_MAX], *p; struct mg_request_info ri; struct cgi_environment blk; FILE *in = NULL, *out = NULL, *err = NULL; struct mg_file fout = STRUCT_FILE_INITIALIZER; pid_t pid = (pid_t)-1; struct process_control_data *proc = NULL; char *cfg_buffering = conn->dom_ctx->config[CGI_BUFFERING + cgi_config_idx]; int no_buffering = 0; #if defined(USE_TIMERS) double cgi_timeout; if (conn->dom_ctx->config[CGI_TIMEOUT + cgi_config_idx]) { /* Get timeout in seconds */ cgi_timeout = atof(conn->dom_ctx->config[CGI_TIMEOUT + cgi_config_idx]) * 0.001; } else { cgi_timeout = atof(config_options[REQUEST_TIMEOUT].default_value) * 0.001; } #endif if (cfg_buffering != NULL) { if (!mg_strcasecmp(cfg_buffering, "no")) { no_buffering = 1; } } buf = NULL; buflen = conn->phys_ctx->max_request_size; i = prepare_cgi_environment(conn, prog, &blk, cgi_config_idx); if (i != 0) { blk.buf = NULL; blk.var = NULL; goto done; } /* CGI must be executed in its own directory. 'dir' must point to the * directory containing executable program, 'p' must point to the * executable program name relative to 'dir'. */ (void)mg_snprintf(conn, &truncated, dir, sizeof(dir), "%s", prog); if (truncated) { mg_cry_internal(conn, "Error: CGI program \"%s\": Path too long", prog); mg_send_http_error(conn, 500, "Error: %s", "CGI path too long"); goto done; } if ((p = strrchr(dir, '/')) != NULL) { *p++ = '\0'; } else { dir[0] = '.'; dir[1] = '\0'; p = (char *)prog; } if ((pipe(fdin) != 0) || (pipe(fdout) != 0) || (pipe(fderr) != 0)) { status = strerror(ERRNO); mg_cry_internal( conn, "Error: CGI program \"%s\": Can not create CGI pipes: %s", prog, status); mg_send_http_error(conn, 500, "Error: Cannot create CGI pipe: %s", status); goto done; } proc = (struct process_control_data *) mg_malloc_ctx(sizeof(struct process_control_data), conn->phys_ctx); if (proc == NULL) { mg_cry_internal(conn, "Error: CGI program \"%s\": Out or memory", prog); mg_send_http_error(conn, 500, "Error: Out of memory [%s]", prog); goto done; } DEBUG_TRACE("CGI: spawn %s %s\n", dir, p); pid = spawn_process( conn, p, blk.buf, blk.var, fdin, fdout, fderr, dir, cgi_config_idx); if (pid == (pid_t)-1) { status = strerror(ERRNO); mg_cry_internal( conn, "Error: CGI program \"%s\": Can not spawn CGI process: %s", prog, status); mg_send_http_error(conn, 500, "Error: Cannot spawn CGI process"); mg_free(proc); proc = NULL; goto done; } /* Store data in shared process_control_data */ proc->pid = pid; proc->references = 1; #if defined(USE_TIMERS) if (cgi_timeout > 0.0) { proc->references = 2; // Start a timer for CGI timer_add(conn->phys_ctx, cgi_timeout /* in seconds */, 0.0, 1, abort_cgi_process, (void *)proc, NULL); } #endif /* Parent closes only one side of the pipes. * If we don't mark them as closed, close() attempt before * return from this function throws an exception on Windows. * Windows does not like when closed descriptor is closed again. */ (void)close(fdin[0]); (void)close(fdout[1]); (void)close(fderr[1]); fdin[0] = fdout[1] = fderr[1] = -1; if (((in = fdopen(fdin[1], "wb")) == NULL) || ((out = fdopen(fdout[0], "rb")) == NULL) || ((err = fdopen(fderr[0], "rb")) == NULL)) { status = strerror(ERRNO); mg_cry_internal(conn, "Error: CGI program \"%s\": Can not open fd: %s", prog, status); mg_send_http_error(conn, 500, "Error: CGI can not open fd\nfdopen: %s", status); goto done; } setbuf(in, NULL); setbuf(out, NULL); setbuf(err, NULL); fout.access.fp = out; if ((conn->content_len != 0) || (conn->is_chunked)) { DEBUG_TRACE("CGI: send body data (%" INT64_FMT ")\n", conn->content_len); /* This is a POST/PUT request, or another request with body data. */ if (!forward_body_data(conn, in, INVALID_SOCKET, NULL)) { /* Error sending the body data */ mg_cry_internal( conn, "Error: CGI program \"%s\": Forward body data failed", prog); goto done; } } /* Close so child gets an EOF. */ fclose(in); in = NULL; fdin[1] = -1; /* Now read CGI reply into a buffer. We need to set correct * status code, thus we need to see all HTTP headers first. * Do not send anything back to client, until we buffer in all * HTTP headers. */ data_len = 0; buf = (char *)mg_malloc_ctx(buflen, conn->phys_ctx); if (buf == NULL) { mg_send_http_error(conn, 500, "Error: Not enough memory for CGI buffer (%u bytes)", (unsigned int)buflen); mg_cry_internal( conn, "Error: CGI program \"%s\": Not enough memory for buffer (%u " "bytes)", prog, (unsigned int)buflen); goto done; } DEBUG_TRACE("CGI: %s", "wait for response"); headers_len = read_message(out, conn, buf, (int)buflen, &data_len); DEBUG_TRACE("CGI: response: %li", (signed long)headers_len); if (headers_len <= 0) { /* Could not parse the CGI response. Check if some error message on * stderr. */ i = pull_all(err, conn, buf, (int)buflen); if (i > 0) { /* CGI program explicitly sent an error */ /* Write the error message to the internal log */ mg_cry_internal(conn, "Error: CGI program \"%s\" sent error " "message: [%.*s]", prog, i, buf); /* Don't send the error message back to the client */ mg_send_http_error(conn, 500, "Error: CGI program \"%s\" failed.", prog); } else { /* CGI program did not explicitly send an error, but a broken * respon header */ mg_cry_internal(conn, "Error: CGI program sent malformed or too big " "(>%u bytes) HTTP headers: [%.*s]", (unsigned)buflen, data_len, buf); mg_send_http_error(conn, 500, "Error: CGI program sent malformed or too big " "(>%u bytes) HTTP headers: [%.*s]", (unsigned)buflen, data_len, buf); } /* in both cases, abort processing CGI */ goto done; } pbuf = buf; buf[headers_len - 1] = '\0'; ri.num_headers = parse_http_headers(&pbuf, ri.http_headers); /* Make up and send the status line */ status_text = "OK"; if ((status = get_header(ri.http_headers, ri.num_headers, "Status")) != NULL) { conn->status_code = atoi(status); status_text = status; while (isdigit((unsigned char)*status_text) || *status_text == ' ') { status_text++; } } else if (get_header(ri.http_headers, ri.num_headers, "Location") != NULL) { conn->status_code = 307; } else { conn->status_code = 200; } if (!should_keep_alive(conn)) { conn->must_close = 1; } DEBUG_TRACE("CGI: response %u %s", conn->status_code, status_text); (void)mg_printf(conn, "HTTP/1.1 %d %s\r\n", conn->status_code, status_text); /* Send headers */ for (i = 0; i < ri.num_headers; i++) { DEBUG_TRACE("CGI header: %s: %s", ri.http_headers[i].name, ri.http_headers[i].value); mg_printf(conn, "%s: %s\r\n", ri.http_headers[i].name, ri.http_headers[i].value); } mg_write(conn, "\r\n", 2); /* Send chunk of data that may have been read after the headers */ mg_write(conn, buf + headers_len, (size_t)(data_len - headers_len)); /* Read the rest of CGI output and send to the client */ DEBUG_TRACE("CGI: %s", "forward all data"); send_file_data(conn, &fout, 0, INT64_MAX, no_buffering); /* send CGI data */ DEBUG_TRACE("CGI: %s", "all data sent"); done: mg_free(blk.var); mg_free(blk.buf); if (pid != (pid_t)-1) { abort_cgi_process((void *)proc); } if (fdin[0] != -1) { close(fdin[0]); } if (fdout[1] != -1) { close(fdout[1]); } if (fderr[1] != -1) { close(fderr[1]); } if (in != NULL) { fclose(in); } else if (fdin[1] != -1) { close(fdin[1]); } if (out != NULL) { fclose(out); } else if (fdout[0] != -1) { close(fdout[0]); } if (err != NULL) { fclose(err); } else if (fderr[0] != -1) { close(fderr[0]); } mg_free(buf); } #endif /* !NO_CGI */ #if !defined(NO_FILES) static void dav_mkcol(struct mg_connection *conn, const char *path) { int rc, body_len; struct de de; if (conn == NULL) { return; } /* TODO (mid): Check the mg_send_http_error situations in this function */ memset(&de.file, 0, sizeof(de.file)); if (!mg_stat(conn, path, &de.file)) { mg_cry_internal(conn, "%s: mg_stat(%s) failed: %s", __func__, path, strerror(ERRNO)); } if (de.file.last_modified) { /* TODO (mid): This check does not seem to make any sense ! */ /* TODO (mid): Add a webdav unit test first, before changing * anything here. */ mg_send_http_error( conn, 405, "Error: mkcol(%s): %s", path, strerror(ERRNO)); return; } body_len = conn->data_len - conn->request_len; if (body_len > 0) { mg_send_http_error( conn, 415, "Error: mkcol(%s): %s", path, strerror(ERRNO)); return; } rc = mg_mkdir(conn, path, 0755); DEBUG_TRACE("mkdir %s: %i", path, rc); if (rc == 0) { /* Create 201 "Created" response */ mg_response_header_start(conn, 201); send_static_cache_header(conn); send_additional_header(conn); mg_response_header_add(conn, "Content-Length", "0", -1); /* Send all headers - there is no body */ mg_response_header_send(conn); } else { int http_status = 500; switch (errno) { case EEXIST: http_status = 405; break; case EACCES: http_status = 403; break; case ENOENT: http_status = 409; break; } mg_send_http_error(conn, http_status, "Error processing %s: %s", path, strerror(ERRNO)); } } /* Forward decrlaration */ static int get_uri_type(const char *uri); static const char * get_rel_url_at_current_server(const char *uri, const struct mg_connection *conn); static void dav_move_file(struct mg_connection *conn, const char *path, int do_copy) { const char *overwrite_hdr; const char *destination_hdr; const char *root; int rc, dest_uri_type; int http_status = 400; int do_overwrite = 0; int destination_ok = 0; char dest_path[UTF8_PATH_MAX]; struct mg_file_stat ignored; if (conn == NULL) { return; } root = conn->dom_ctx->config[DOCUMENT_ROOT]; overwrite_hdr = mg_get_header(conn, "Overwrite"); destination_hdr = mg_get_header(conn, "Destination"); if ((overwrite_hdr != NULL) && (toupper(overwrite_hdr[0]) == 'T')) { do_overwrite = 1; } if ((destination_hdr == NULL) || (destination_hdr[0] == 0)) { mg_send_http_error(conn, 400, "%s", "Missing destination"); return; } if (root != NULL) { char *local_dest = NULL; dest_uri_type = get_uri_type(destination_hdr); if (dest_uri_type == 2) { local_dest = mg_strdup_ctx(destination_hdr, conn->phys_ctx); } else if ((dest_uri_type == 3) || (dest_uri_type == 4)) { const char *h = get_rel_url_at_current_server(destination_hdr, conn); if (h) { size_t len = strlen(h); local_dest = mg_malloc_ctx(len + 1, conn->phys_ctx); mg_url_decode(h, (int)len, local_dest, (int)len + 1, 0); } } if (local_dest != NULL) { remove_dot_segments(local_dest); if (local_dest[0] == '/') { int trunc_check = 0; mg_snprintf(conn, &trunc_check, dest_path, sizeof(dest_path), "%s/%s", root, local_dest); if (trunc_check == 0) { destination_ok = 1; } } mg_free(local_dest); } } if (!destination_ok) { mg_send_http_error(conn, 502, "%s", "Illegal destination"); return; } /* Check now if this file exists */ if (mg_stat(conn, dest_path, &ignored)) { /* File exists */ if (do_overwrite) { /* Overwrite allowed: delete the file first */ if (0 != remove(dest_path)) { /* No overwrite: return error */ mg_send_http_error(conn, 403, "Cannot overwrite file: %s", dest_path); return; } } else { /* No overwrite: return error */ mg_send_http_error(conn, 412, "Destination already exists: %s", dest_path); return; } } /* Copy / Move / Rename operation. */ DEBUG_TRACE("%s %s to %s", (do_copy ? "copy" : "move"), path, dest_path); #if defined(_WIN32) { /* For Windows, we need to convert from UTF-8 to UTF-16 first. */ wchar_t wSource[UTF16_PATH_MAX]; wchar_t wDest[UTF16_PATH_MAX]; BOOL ok; path_to_unicode(conn, path, wSource, ARRAY_SIZE(wSource)); path_to_unicode(conn, dest_path, wDest, ARRAY_SIZE(wDest)); if (do_copy) { ok = CopyFileW(wSource, wDest, do_overwrite ? FALSE : TRUE); } else { ok = MoveFileExW(wSource, wDest, do_overwrite ? MOVEFILE_REPLACE_EXISTING : 0); } if (ok) { rc = 0; } else { DWORD lastErr = GetLastError(); if (lastErr == ERROR_ALREADY_EXISTS) { mg_send_http_error(conn, 412, "Destination already exists: %s", dest_path); return; } rc = -1; http_status = 400; } } #else { /* Linux uses already UTF-8, we don't need to convert file names. */ if (do_copy) { /* TODO: COPY for Linux. */ mg_send_http_error(conn, 403, "%s", "COPY forbidden"); return; } rc = rename(path, dest_path); if (rc) { switch (errno) { case EEXIST: http_status = 412; break; case EACCES: http_status = 403; break; case ENOENT: http_status = 409; break; } } } #endif if (rc == 0) { /* Create 204 "No Content" response */ mg_response_header_start(conn, 204); mg_response_header_add(conn, "Content-Length", "0", -1); /* Send all headers - there is no body */ mg_response_header_send(conn); } else { mg_send_http_error(conn, http_status, "Operation failed"); } } static void put_file(struct mg_connection *conn, const char *path) { struct mg_file file = STRUCT_FILE_INITIALIZER; const char *range; int64_t r1, r2; int rc; if (conn == NULL) { return; } DEBUG_TRACE("store %s", path); if (mg_stat(conn, path, &file.stat)) { /* File already exists */ conn->status_code = 200; if (file.stat.is_directory) { /* This is an already existing directory, * so there is nothing to do for the server. */ rc = 0; } else { /* File exists and is not a directory. */ /* Can it be replaced? */ /* Check if the server may write this file */ if (access(path, W_OK) == 0) { /* Access granted */ rc = 1; } else { mg_send_http_error( conn, 403, "Error: Put not possible\nReplacing %s is not allowed", path); return; } } } else { /* File should be created */ conn->status_code = 201; rc = put_dir(conn, path); } if (rc == 0) { /* put_dir returns 0 if path is a directory */ /* Create response */ mg_response_header_start(conn, conn->status_code); send_no_cache_header(conn); send_additional_header(conn); mg_response_header_add(conn, "Content-Length", "0", -1); /* Send all headers - there is no body */ mg_response_header_send(conn); /* Request to create a directory has been fulfilled successfully. * No need to put a file. */ return; } if (rc == -1) { /* put_dir returns -1 if the path is too long */ mg_send_http_error(conn, 414, "Error: Path too long\nput_dir(%s): %s", path, strerror(ERRNO)); return; } if (rc == -2) { /* put_dir returns -2 if the directory can not be created */ mg_send_http_error(conn, 500, "Error: Can not create directory\nput_dir(%s): %s", path, strerror(ERRNO)); return; } /* A file should be created or overwritten. */ /* Currently CivetWeb does not need read+write access. */ if (!mg_fopen(conn, path, MG_FOPEN_MODE_WRITE, &file) || file.access.fp == NULL) { (void)mg_fclose(&file.access); mg_send_http_error(conn, 500, "Error: Can not create file\nfopen(%s): %s", path, strerror(ERRNO)); return; } fclose_on_exec(&file.access, conn); range = mg_get_header(conn, "Content-Range"); r1 = r2 = 0; if ((range != NULL) && parse_range_header(range, &r1, &r2) > 0) { conn->status_code = 206; /* Partial content */ if (0 != fseeko(file.access.fp, r1, SEEK_SET)) { mg_send_http_error(conn, 500, "Error: Internal error processing file %s", path); return; } } if (!forward_body_data(conn, file.access.fp, INVALID_SOCKET, NULL)) { /* forward_body_data failed. * The error code has already been sent to the client, * and conn->status_code is already set. */ (void)mg_fclose(&file.access); return; } if (mg_fclose(&file.access) != 0) { /* fclose failed. This might have different reasons, but a likely * one is "no space on disk", http 507. */ conn->status_code = 507; } /* Create response (status_code has been set before) */ mg_response_header_start(conn, conn->status_code); send_no_cache_header(conn); send_additional_header(conn); mg_response_header_add(conn, "Content-Length", "0", -1); /* Send all headers - there is no body */ mg_response_header_send(conn); } static void delete_file(struct mg_connection *conn, const char *path) { struct de de; memset(&de.file, 0, sizeof(de.file)); if (!mg_stat(conn, path, &de.file)) { /* mg_stat returns 0 if the file does not exist */ mg_send_http_error(conn, 404, "Error: Cannot delete file\nFile %s not found", path); return; } DEBUG_TRACE("delete %s", path); if (de.file.is_directory) { if (remove_directory(conn, path)) { /* Delete is successful: Return 204 without content. */ mg_send_http_error(conn, 204, "%s", ""); } else { /* Delete is not successful: Return 500 (Server error). */ mg_send_http_error(conn, 500, "Error: Could not delete %s", path); } return; } /* This is an existing file (not a directory). * Check if write permission is granted. */ if (access(path, W_OK) != 0) { /* File is read only */ mg_send_http_error( conn, 403, "Error: Delete not possible\nDeleting %s is not allowed", path); return; } /* Try to delete it. */ if (mg_remove(conn, path) == 0) { /* Delete was successful: Return 204 without content. */ mg_response_header_start(conn, 204); send_no_cache_header(conn); send_additional_header(conn); mg_response_header_add(conn, "Content-Length", "0", -1); mg_response_header_send(conn); } else { /* Delete not successful (file locked). */ mg_send_http_error(conn, 423, "Error: Cannot delete file\nremove(%s): %s", path, strerror(ERRNO)); } } #endif /* !NO_FILES */ #if !defined(NO_FILESYSTEMS) static void send_ssi_file(struct mg_connection *, const char *, struct mg_file *, int); static void do_ssi_include(struct mg_connection *conn, const char *ssi, char *tag, int include_level) { char file_name[MG_BUF_LEN], path[512], *p; struct mg_file file = STRUCT_FILE_INITIALIZER; size_t len; int truncated = 0; if (conn == NULL) { return; } /* sscanf() is safe here, since send_ssi_file() also uses buffer * of size MG_BUF_LEN to get the tag. So strlen(tag) is * always < MG_BUF_LEN. */ if (sscanf(tag, " virtual=\"%511[^\"]\"", file_name) == 1) { /* File name is relative to the webserver root */ file_name[511] = 0; (void)mg_snprintf(conn, &truncated, path, sizeof(path), "%s/%s", conn->dom_ctx->config[DOCUMENT_ROOT], file_name); } else if (sscanf(tag, " abspath=\"%511[^\"]\"", file_name) == 1) { /* File name is relative to the webserver working directory * or it is absolute system path */ file_name[511] = 0; (void) mg_snprintf(conn, &truncated, path, sizeof(path), "%s", file_name); } else if ((sscanf(tag, " file=\"%511[^\"]\"", file_name) == 1) || (sscanf(tag, " \"%511[^\"]\"", file_name) == 1)) { /* File name is relative to the current document */ file_name[511] = 0; (void)mg_snprintf(conn, &truncated, path, sizeof(path), "%s", ssi); if (!truncated) { if ((p = strrchr(path, '/')) != NULL) { p[1] = '\0'; } len = strlen(path); (void)mg_snprintf(conn, &truncated, path + len, sizeof(path) - len, "%s", file_name); } } else { mg_cry_internal(conn, "Bad SSI #include: [%s]", tag); return; } if (truncated) { mg_cry_internal(conn, "SSI #include path length overflow: [%s]", tag); return; } if (!mg_fopen(conn, path, MG_FOPEN_MODE_READ, &file)) { mg_cry_internal(conn, "Cannot open SSI #include: [%s]: fopen(%s): %s", tag, path, strerror(ERRNO)); } else { fclose_on_exec(&file.access, conn); if (match_prefix_strlen(conn->dom_ctx->config[SSI_EXTENSIONS], path) > 0) { send_ssi_file(conn, path, &file, include_level + 1); } else { send_file_data(conn, &file, 0, INT64_MAX, 0); /* send static file */ } (void)mg_fclose(&file.access); /* Ignore errors for readonly files */ } } #if !defined(NO_POPEN) static void do_ssi_exec(struct mg_connection *conn, char *tag) { char cmd[1024] = ""; struct mg_file file = STRUCT_FILE_INITIALIZER; if (sscanf(tag, " \"%1023[^\"]\"", cmd) != 1) { mg_cry_internal(conn, "Bad SSI #exec: [%s]", tag); } else { cmd[1023] = 0; if ((file.access.fp = popen(cmd, "r")) == NULL) { mg_cry_internal(conn, "Cannot SSI #exec: [%s]: %s", cmd, strerror(ERRNO)); } else { send_file_data(conn, &file, 0, INT64_MAX, 0); /* send static file */ pclose(file.access.fp); } } } #endif /* !NO_POPEN */ static int mg_fgetc(struct mg_file *filep) { if (filep == NULL) { return EOF; } if (filep->access.fp != NULL) { return fgetc(filep->access.fp); } else { return EOF; } } static void send_ssi_file(struct mg_connection *conn, const char *path, struct mg_file *filep, int include_level) { char buf[MG_BUF_LEN]; int ch, len, in_tag, in_ssi_tag; if (include_level > 10) { mg_cry_internal(conn, "SSI #include level is too deep (%s)", path); return; } in_tag = in_ssi_tag = len = 0; /* Read file, byte by byte, and look for SSI include tags */ while ((ch = mg_fgetc(filep)) != EOF) { if (in_tag) { /* We are in a tag, either SSI tag or html tag */ if (ch == '>') { /* Tag is closing */ buf[len++] = '>'; if (in_ssi_tag) { /* Handle SSI tag */ buf[len] = 0; if ((len > 12) && !memcmp(buf + 5, "include", 7)) { do_ssi_include(conn, path, buf + 12, include_level + 1); #if !defined(NO_POPEN) } else if ((len > 9) && !memcmp(buf + 5, "exec", 4)) { do_ssi_exec(conn, buf + 9); #endif /* !NO_POPEN */ } else { mg_cry_internal(conn, "%s: unknown SSI " "command: \"%s\"", path, buf); } len = 0; in_ssi_tag = in_tag = 0; } else { /* Not an SSI tag */ /* Flush buffer */ (void)mg_write(conn, buf, (size_t)len); len = 0; in_tag = 0; } } else { /* Tag is still open */ buf[len++] = (char)(ch & 0xff); if ((len == 5) && !memcmp(buf, " Error */ return -1; } /* Upgrade to ... */ if (0 != mg_strcasestr(upgrade_to, "websocket")) { /* The headers "Host", "Sec-WebSocket-Key", "Sec-WebSocket-Protocol" and * "Sec-WebSocket-Version" are also required. * Don't check them here, since even an unsupported websocket protocol * request still IS a websocket request (in contrast to a standard HTTP * request). It will fail later in handle_websocket_request. */ return PROTOCOL_TYPE_WEBSOCKET; /* Websocket */ } if (0 != mg_strcasestr(upgrade_to, "h2")) { return PROTOCOL_TYPE_HTTP2; /* Websocket */ } /* Upgrade to another protocol */ return -1; } static int parse_match_net(const struct vec *vec, const union usa *sa, int no_strict) { int n; unsigned int a, b, c, d, slash; if (sscanf(vec->ptr, "%u.%u.%u.%u/%u%n", &a, &b, &c, &d, &slash, &n) != 5) { // NOLINT(cert-err34-c) 'sscanf' used to convert a string to an // integer value, but function will not report conversion // errors; consider using 'strtol' instead slash = 32; if (sscanf(vec->ptr, "%u.%u.%u.%u%n", &a, &b, &c, &d, &n) != 4) { // NOLINT(cert-err34-c) 'sscanf' used to convert a string to // an integer value, but function will not report conversion // errors; consider using 'strtol' instead n = 0; } } if ((n > 0) && ((size_t)n == vec->len)) { if ((a < 256) && (b < 256) && (c < 256) && (d < 256) && (slash < 33)) { /* IPv4 format */ if (sa->sa.sa_family == AF_INET) { uint32_t ip = ntohl(sa->sin.sin_addr.s_addr); uint32_t net = ((uint32_t)a << 24) | ((uint32_t)b << 16) | ((uint32_t)c << 8) | (uint32_t)d; uint32_t mask = slash ? (0xFFFFFFFFu << (32 - slash)) : 0; return (ip & mask) == net; } return 0; } } #if defined(USE_IPV6) else { char ad[50]; const char *p; if (sscanf(vec->ptr, "[%49[^]]]/%u%n", ad, &slash, &n) != 2) { slash = 128; if (sscanf(vec->ptr, "[%49[^]]]%n", ad, &n) != 1) { n = 0; } } if ((n <= 0) && no_strict) { /* no square brackets? */ p = strchr(vec->ptr, '/'); if (p && (p < (vec->ptr + vec->len))) { if (((size_t)(p - vec->ptr) < sizeof(ad)) && (sscanf(p, "/%u%n", &slash, &n) == 1)) { n += (int)(p - vec->ptr); mg_strlcpy(ad, vec->ptr, (size_t)(p - vec->ptr) + 1); } else { n = 0; } } else if (vec->len < sizeof(ad)) { n = (int)vec->len; slash = 128; mg_strlcpy(ad, vec->ptr, vec->len + 1); } } if ((n > 0) && ((size_t)n == vec->len) && (slash < 129)) { p = ad; c = 0; /* zone indexes are unsupported, at least two colons are needed */ while (isxdigit((unsigned char)*p) || (*p == '.') || (*p == ':')) { if (*(p++) == ':') { c++; } } if ((*p == '\0') && (c >= 2)) { struct sockaddr_in6 sin6; unsigned int i; /* for strict validation, an actual IPv6 argument is needed */ if (sa->sa.sa_family != AF_INET6) { return 0; } if (mg_inet_pton(AF_INET6, ad, &sin6, sizeof(sin6), 0)) { /* IPv6 format */ for (i = 0; i < 16; i++) { uint8_t ip = sa->sin6.sin6_addr.s6_addr[i]; uint8_t net = sin6.sin6_addr.s6_addr[i]; uint8_t mask = 0; if (8 * i + 8 < slash) { mask = 0xFFu; } else if (8 * i < slash) { mask = (uint8_t)(0xFFu << (8 * i + 8 - slash)); } if ((ip & mask) != net) { return 0; } } return 1; } } } } #else (void)no_strict; #endif /* malformed */ return -1; } static int set_throttle(const char *spec, const union usa *rsa, const char *uri) { int throttle = 0; struct vec vec, val; char mult; double v; while ((spec = next_option(spec, &vec, &val)) != NULL) { mult = ','; if ((val.ptr == NULL) || (sscanf(val.ptr, "%lf%c", &v, &mult) < 1) // NOLINT(cert-err34-c) 'sscanf' used to convert a string // to an integer value, but function will not report // conversion errors; consider using 'strtol' instead || (v < 0) || ((lowercase(&mult) != 'k') && (lowercase(&mult) != 'm') && (mult != ','))) { continue; } v *= (lowercase(&mult) == 'k') ? 1024 : ((lowercase(&mult) == 'm') ? 1048576 : 1); if (vec.len == 1 && vec.ptr[0] == '*') { throttle = (int)v; } else { int matched = parse_match_net(&vec, rsa, 0); if (matched >= 0) { /* a valid IP subnet */ if (matched) { throttle = (int)v; } } else if (match_prefix(vec.ptr, vec.len, uri) > 0) { throttle = (int)v; } } } return throttle; } /* The mg_upload function is superseded by mg_handle_form_request. */ #include "handle_form.h" static int get_first_ssl_listener_index(const struct mg_context *ctx) { unsigned int i; int idx = -1; if (ctx) { for (i = 0; ((idx == -1) && (i < ctx->num_listening_sockets)); i++) { idx = ctx->listening_sockets[i].is_ssl ? ((int)(i)) : -1; } } return idx; } /* Return host (without port) */ static void get_host_from_request_info(struct vec *host, const struct mg_request_info *ri) { const char *host_header = get_header(ri->http_headers, ri->num_headers, "Host"); host->ptr = NULL; host->len = 0; if (host_header != NULL) { const char *pos; /* If the "Host" is an IPv6 address, like [::1], parse until ] * is found. */ if (*host_header == '[') { pos = strchr(host_header, ']'); if (!pos) { /* Malformed hostname starts with '[', but no ']' found */ DEBUG_TRACE("%s", "Host name format error '[' without ']'"); return; } /* terminate after ']' */ host->ptr = host_header; host->len = (size_t)(pos + 1 - host_header); } else { /* Otherwise, a ':' separates hostname and port number */ pos = strchr(host_header, ':'); if (pos != NULL) { host->len = (size_t)(pos - host_header); } else { host->len = strlen(host_header); } host->ptr = host_header; } } } static int switch_domain_context(struct mg_connection *conn) { struct vec host; get_host_from_request_info(&host, &conn->request_info); if (host.ptr) { if (conn->ssl) { /* This is a HTTPS connection, maybe we have a hostname * from SNI (set in ssl_servername_callback). */ const char *sslhost = conn->dom_ctx->config[AUTHENTICATION_DOMAIN]; if (sslhost && (conn->dom_ctx != &(conn->phys_ctx->dd))) { /* We are not using the default domain */ if ((strlen(sslhost) != host.len) || mg_strncasecmp(host.ptr, sslhost, host.len)) { /* Mismatch between SNI domain and HTTP domain */ DEBUG_TRACE("Host mismatch: SNI: %s, HTTPS: %.*s", sslhost, (int)host.len, host.ptr); return 0; } } } else { struct mg_domain_context *dom = &(conn->phys_ctx->dd); while (dom) { const char *domName = dom->config[AUTHENTICATION_DOMAIN]; size_t domNameLen = strlen(domName); if ((domNameLen == host.len) && !mg_strncasecmp(host.ptr, domName, host.len)) { /* Found matching domain */ DEBUG_TRACE("HTTP domain %s found", dom->config[AUTHENTICATION_DOMAIN]); /* TODO: Check if this is a HTTP or HTTPS domain */ conn->dom_ctx = dom; break; } mg_lock_context(conn->phys_ctx); dom = dom->next; mg_unlock_context(conn->phys_ctx); } } DEBUG_TRACE("HTTP%s Host: %.*s", conn->ssl ? "S" : "", (int)host.len, host.ptr); } else { DEBUG_TRACE("HTTP%s Host is not set", conn->ssl ? "S" : ""); return 1; } return 1; } static void redirect_to_https_port(struct mg_connection *conn, int port) { char target_url[MG_BUF_LEN]; int truncated = 0; const char *expect_proto = (conn->protocol_type == PROTOCOL_TYPE_WEBSOCKET) ? "wss" : "https"; /* Use "308 Permanent Redirect" */ int redirect_code = 308; /* In any case, close the current connection */ conn->must_close = 1; /* Send host, port, uri and (if it exists) ?query_string */ if (mg_construct_local_link( conn, target_url, sizeof(target_url), expect_proto, port, NULL) < 0) { truncated = 1; } else if (conn->request_info.query_string != NULL) { size_t slen1 = strlen(target_url); size_t slen2 = strlen(conn->request_info.query_string); if ((slen1 + slen2 + 2) < sizeof(target_url)) { target_url[slen1] = '?'; memcpy(target_url + slen1 + 1, conn->request_info.query_string, slen2); target_url[slen1 + slen2 + 1] = 0; } else { truncated = 1; } } /* Check overflow in location buffer (will not occur if MG_BUF_LEN * is used as buffer size) */ if (truncated) { mg_send_http_error(conn, 500, "%s", "Redirect URL too long"); return; } /* Use redirect helper function */ mg_send_http_redirect(conn, target_url, redirect_code); } static void mg_set_handler_type(struct mg_context *phys_ctx, struct mg_domain_context *dom_ctx, const char *uri, int handler_type, int is_delete_request, mg_request_handler handler, struct mg_websocket_subprotocols *subprotocols, mg_websocket_connect_handler connect_handler, mg_websocket_ready_handler ready_handler, mg_websocket_data_handler data_handler, mg_websocket_close_handler close_handler, mg_authorization_handler auth_handler, void *cbdata) { struct mg_handler_info *tmp_rh, **lastref; size_t urilen = strlen(uri); if (handler_type == WEBSOCKET_HANDLER) { DEBUG_ASSERT(handler == NULL); DEBUG_ASSERT(is_delete_request || connect_handler != NULL || ready_handler != NULL || data_handler != NULL || close_handler != NULL); DEBUG_ASSERT(auth_handler == NULL); if (handler != NULL) { return; } if (!is_delete_request && (connect_handler == NULL) && (ready_handler == NULL) && (data_handler == NULL) && (close_handler == NULL)) { return; } if (auth_handler != NULL) { return; } } else if (handler_type == REQUEST_HANDLER) { DEBUG_ASSERT(connect_handler == NULL && ready_handler == NULL && data_handler == NULL && close_handler == NULL); DEBUG_ASSERT(is_delete_request || (handler != NULL)); DEBUG_ASSERT(auth_handler == NULL); if ((connect_handler != NULL) || (ready_handler != NULL) || (data_handler != NULL) || (close_handler != NULL)) { return; } if (!is_delete_request && (handler == NULL)) { return; } if (auth_handler != NULL) { return; } } else if (handler_type == AUTH_HANDLER) { DEBUG_ASSERT(handler == NULL); DEBUG_ASSERT(connect_handler == NULL && ready_handler == NULL && data_handler == NULL && close_handler == NULL); DEBUG_ASSERT(is_delete_request || (auth_handler != NULL)); if (handler != NULL) { return; } if ((connect_handler != NULL) || (ready_handler != NULL) || (data_handler != NULL) || (close_handler != NULL)) { return; } if (!is_delete_request && (auth_handler == NULL)) { return; } } else { /* Unknown handler type. */ return; } if (!phys_ctx || !dom_ctx) { /* no context available */ return; } mg_lock_context(phys_ctx); /* first try to find an existing handler */ do { lastref = &(dom_ctx->handlers); for (tmp_rh = dom_ctx->handlers; tmp_rh != NULL; tmp_rh = tmp_rh->next) { if (tmp_rh->handler_type == handler_type && (urilen == tmp_rh->uri_len) && !strcmp(tmp_rh->uri, uri)) { if (!is_delete_request) { /* update existing handler */ if (handler_type == REQUEST_HANDLER) { /* Wait for end of use before updating */ if (tmp_rh->refcount) { mg_unlock_context(phys_ctx); mg_sleep(1); mg_lock_context(phys_ctx); /* tmp_rh might have been freed, search again. */ break; } /* Ok, the handler is no more use -> Update it */ tmp_rh->handler = handler; } else if (handler_type == WEBSOCKET_HANDLER) { tmp_rh->subprotocols = subprotocols; tmp_rh->connect_handler = connect_handler; tmp_rh->ready_handler = ready_handler; tmp_rh->data_handler = data_handler; tmp_rh->close_handler = close_handler; } else { /* AUTH_HANDLER */ tmp_rh->auth_handler = auth_handler; } tmp_rh->cbdata = cbdata; } else { /* remove existing handler */ if (handler_type == REQUEST_HANDLER) { /* Wait for end of use before removing */ if (tmp_rh->refcount) { tmp_rh->removing = 1; mg_unlock_context(phys_ctx); mg_sleep(1); mg_lock_context(phys_ctx); /* tmp_rh might have been freed, search again. */ break; } /* Ok, the handler is no more used */ } *lastref = tmp_rh->next; mg_free(tmp_rh->uri); mg_free(tmp_rh); } mg_unlock_context(phys_ctx); return; } lastref = &(tmp_rh->next); } } while (tmp_rh != NULL); if (is_delete_request) { /* no handler to set, this was a remove request to a non-existing * handler */ mg_unlock_context(phys_ctx); return; } tmp_rh = (struct mg_handler_info *)mg_calloc_ctx(1, sizeof(struct mg_handler_info), phys_ctx); if (tmp_rh == NULL) { mg_unlock_context(phys_ctx); mg_cry_ctx_internal(phys_ctx, "%s", "Cannot create new request handler struct, OOM"); return; } tmp_rh->uri = mg_strdup_ctx(uri, phys_ctx); if (!tmp_rh->uri) { mg_unlock_context(phys_ctx); mg_free(tmp_rh); mg_cry_ctx_internal(phys_ctx, "%s", "Cannot create new request handler struct, OOM"); return; } tmp_rh->uri_len = urilen; if (handler_type == REQUEST_HANDLER) { tmp_rh->refcount = 0; tmp_rh->removing = 0; tmp_rh->handler = handler; } else if (handler_type == WEBSOCKET_HANDLER) { tmp_rh->subprotocols = subprotocols; tmp_rh->connect_handler = connect_handler; tmp_rh->ready_handler = ready_handler; tmp_rh->data_handler = data_handler; tmp_rh->close_handler = close_handler; } else { /* AUTH_HANDLER */ tmp_rh->auth_handler = auth_handler; } tmp_rh->cbdata = cbdata; tmp_rh->handler_type = handler_type; tmp_rh->next = NULL; *lastref = tmp_rh; mg_unlock_context(phys_ctx); } CIVETWEB_API void mg_set_request_handler(struct mg_context *ctx, const char *uri, mg_request_handler handler, void *cbdata) { mg_set_handler_type(ctx, &(ctx->dd), uri, REQUEST_HANDLER, handler == NULL, handler, NULL, NULL, NULL, NULL, NULL, NULL, cbdata); } CIVETWEB_API void mg_set_websocket_handler(struct mg_context *ctx, const char *uri, mg_websocket_connect_handler connect_handler, mg_websocket_ready_handler ready_handler, mg_websocket_data_handler data_handler, mg_websocket_close_handler close_handler, void *cbdata) { mg_set_websocket_handler_with_subprotocols(ctx, uri, NULL, connect_handler, ready_handler, data_handler, close_handler, cbdata); } CIVETWEB_API void mg_set_websocket_handler_with_subprotocols( struct mg_context *ctx, const char *uri, struct mg_websocket_subprotocols *subprotocols, mg_websocket_connect_handler connect_handler, mg_websocket_ready_handler ready_handler, mg_websocket_data_handler data_handler, mg_websocket_close_handler close_handler, void *cbdata) { int is_delete_request = (connect_handler == NULL) && (ready_handler == NULL) && (data_handler == NULL) && (close_handler == NULL); mg_set_handler_type(ctx, &(ctx->dd), uri, WEBSOCKET_HANDLER, is_delete_request, NULL, subprotocols, connect_handler, ready_handler, data_handler, close_handler, NULL, cbdata); } CIVETWEB_API void mg_set_auth_handler(struct mg_context *ctx, const char *uri, mg_authorization_handler handler, void *cbdata) { mg_set_handler_type(ctx, &(ctx->dd), uri, AUTH_HANDLER, handler == NULL, NULL, NULL, NULL, NULL, NULL, NULL, handler, cbdata); } static int get_request_handler(struct mg_connection *conn, int handler_type, mg_request_handler *handler, struct mg_websocket_subprotocols **subprotocols, mg_websocket_connect_handler *connect_handler, mg_websocket_ready_handler *ready_handler, mg_websocket_data_handler *data_handler, mg_websocket_close_handler *close_handler, mg_authorization_handler *auth_handler, void **cbdata, struct mg_handler_info **handler_info) { const struct mg_request_info *request_info = mg_get_request_info(conn); if (request_info) { const char *uri = request_info->local_uri; size_t urilen = strlen(uri); struct mg_handler_info *tmp_rh; int step, matched; if (!conn || !conn->phys_ctx || !conn->dom_ctx) { return 0; } mg_lock_context(conn->phys_ctx); for (step = 0; step < 3; step++) { for (tmp_rh = conn->dom_ctx->handlers; tmp_rh != NULL; tmp_rh = tmp_rh->next) { if (tmp_rh->handler_type != handler_type) { continue; } if (step == 0) { /* first try for an exact match */ matched = (tmp_rh->uri_len == urilen) && (strcmp(tmp_rh->uri, uri) == 0); } else if (step == 1) { /* next try for a partial match, we will accept uri/something */ matched = (tmp_rh->uri_len < urilen) && (uri[tmp_rh->uri_len] == '/') && (memcmp(tmp_rh->uri, uri, tmp_rh->uri_len) == 0); } else { /* finally try for pattern match */ matched = match_prefix(tmp_rh->uri, tmp_rh->uri_len, uri) > 0; } if (matched) { if (handler_type == WEBSOCKET_HANDLER) { *subprotocols = tmp_rh->subprotocols; *connect_handler = tmp_rh->connect_handler; *ready_handler = tmp_rh->ready_handler; *data_handler = tmp_rh->data_handler; *close_handler = tmp_rh->close_handler; } else if (handler_type == REQUEST_HANDLER) { if (tmp_rh->removing) { /* Treat as none found */ step = 2; break; } *handler = tmp_rh->handler; /* Acquire handler and give it back */ tmp_rh->refcount++; *handler_info = tmp_rh; } else { /* AUTH_HANDLER */ *auth_handler = tmp_rh->auth_handler; } *cbdata = tmp_rh->cbdata; mg_unlock_context(conn->phys_ctx); return 1; } } } mg_unlock_context(conn->phys_ctx); } return 0; /* none found */ } /* Check if the script file is in a path, allowed for script files. * This can be used if uploading files is possible not only for the server * admin, and the upload mechanism does not check the file extension. */ static int is_in_script_path(const struct mg_connection *conn, const char *path) { /* TODO (Feature): Add config value for allowed script path. * Default: All allowed. */ (void)conn; (void)path; return 1; } #if defined(USE_WEBSOCKET) && defined(MG_EXPERIMENTAL_INTERFACES) static int experimental_websocket_client_data_wrapper(struct mg_connection *conn, int bits, char *data, size_t len, void *cbdata) { struct mg_callbacks *pcallbacks = (struct mg_callbacks *)cbdata; if (pcallbacks->websocket_data) { return pcallbacks->websocket_data(conn, bits, data, len); } /* No handler set - assume "OK" */ return 1; } static void experimental_websocket_client_close_wrapper(const struct mg_connection *conn, void *cbdata) { struct mg_callbacks *pcallbacks = (struct mg_callbacks *)cbdata; if (pcallbacks->connection_close) { pcallbacks->connection_close(conn); } } #endif /* Decrement recount of handler. conn must not be NULL, handler_info may be NULL */ static void release_handler_ref(struct mg_connection *conn, struct mg_handler_info *handler_info) { if (handler_info != NULL) { /* Use context lock for ref counter */ mg_lock_context(conn->phys_ctx); handler_info->refcount--; mg_unlock_context(conn->phys_ctx); } } /* This is the heart of the Civetweb's logic. * This function is called when the request is read, parsed and validated, * and Civetweb must decide what action to take: serve a file, or * a directory, or call embedded function, etcetera. */ static void handle_request(struct mg_connection *conn) { struct mg_request_info *ri = &conn->request_info; char path[UTF8_PATH_MAX]; int uri_len, ssl_index; int is_found = 0, is_script_resource = 0, is_websocket_request = 0, is_put_or_delete_request = 0, is_callback_resource = 0, is_template_text_file = 0, is_webdav_request = 0; int i; struct mg_file file = STRUCT_FILE_INITIALIZER; mg_request_handler callback_handler = NULL; struct mg_handler_info *handler_info = NULL; struct mg_websocket_subprotocols *subprotocols; mg_websocket_connect_handler ws_connect_handler = NULL; mg_websocket_ready_handler ws_ready_handler = NULL; mg_websocket_data_handler ws_data_handler = NULL; mg_websocket_close_handler ws_close_handler = NULL; void *callback_data = NULL; mg_authorization_handler auth_handler = NULL; void *auth_callback_data = NULL; int handler_type; time_t curtime = time(NULL); char date[64]; char *tmp; path[0] = 0; /* 0. Reset internal state (required for HTTP/2 proxy) */ conn->request_state = 0; /* 1. get the request url */ /* 1.1. split into url and query string */ if ((conn->request_info.query_string = strchr(ri->request_uri, '?')) != NULL) { *((char *)conn->request_info.query_string++) = '\0'; } /* 1.2. do a https redirect, if required. Do not decode URIs yet. */ if (!conn->client.is_ssl && conn->client.ssl_redir) { ssl_index = get_first_ssl_listener_index(conn->phys_ctx); if (ssl_index >= 0) { int port = (int)ntohs(USA_IN_PORT_UNSAFE( &(conn->phys_ctx->listening_sockets[ssl_index].lsa))); redirect_to_https_port(conn, port); } else { /* A http to https forward port has been specified, * but no https port to forward to. */ mg_send_http_error(conn, 503, "%s", "Error: SSL forward not configured properly"); mg_cry_internal(conn, "%s", "Can not redirect to SSL, no SSL port available"); } return; } uri_len = (int)strlen(ri->local_uri); /* 1.3. decode url (if config says so) */ if (should_decode_url(conn)) { url_decode_in_place((char *)ri->local_uri); } /* URL decode the query-string only if explicitly set in the configuration */ if (conn->request_info.query_string) { if (should_decode_query_string(conn)) { url_decode_in_place((char *)conn->request_info.query_string); } } /* 1.4. clean URIs, so a path like allowed_dir/../forbidden_file is not * possible. The fact that we cleaned the URI is stored in that the * pointer to ri->local_ur and ri->local_uri_raw are now different. * ri->local_uri_raw still points to memory allocated in * worker_thread_run(). ri->local_uri is private to the request so we * don't have to use preallocated memory here. */ tmp = mg_strdup(ri->local_uri_raw); if (!tmp) { /* Out of memory. We cannot do anything reasonable here. */ return; } remove_dot_segments(tmp); ri->local_uri = tmp; /* step 1. completed, the url is known now */ DEBUG_TRACE("REQUEST: %s %s", ri->request_method, ri->local_uri); /* 2. if this ip has limited speed, set it for this connection */ conn->throttle = set_throttle(conn->dom_ctx->config[THROTTLE], &conn->client.rsa, ri->local_uri); /* 3. call a "handle everything" callback, if registered */ if (conn->phys_ctx->callbacks.begin_request != NULL) { /* Note that since V1.7 the "begin_request" function is called * before an authorization check. If an authorization check is * required, use a request_handler instead. */ i = conn->phys_ctx->callbacks.begin_request(conn); if (i > 0) { /* callback already processed the request. Store the return value as a status code for the access log. */ conn->status_code = i; if (!conn->must_close) { discard_unread_request_data(conn); } DEBUG_TRACE("%s", "begin_request handled request"); return; } else if (i == 0) { /* civetweb should process the request */ } else { /* unspecified - may change with the next version */ DEBUG_TRACE("%s", "done (undocumented behavior)"); return; } } /* request not yet handled by a handler or redirect, so the request * is processed here */ /* 4. Check for CORS preflight requests and handle them (if configured). * https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS */ if (!strcmp(ri->request_method, "OPTIONS")) { /* Send a response to CORS preflights only if * access_control_allow_methods is not NULL and not an empty string. * In this case, scripts can still handle CORS. */ const char *cors_meth_cfg = conn->dom_ctx->config[ACCESS_CONTROL_ALLOW_METHODS]; const char *cors_orig_cfg = conn->dom_ctx->config[ACCESS_CONTROL_ALLOW_ORIGIN]; const char *cors_origin = get_header(ri->http_headers, ri->num_headers, "Origin"); const char *cors_acrm = get_header(ri->http_headers, ri->num_headers, "Access-Control-Request-Method"); /* Todo: check if cors_origin is in cors_orig_cfg. * Or, let the client check this. */ if ((cors_meth_cfg != NULL) && (*cors_meth_cfg != 0) && (cors_orig_cfg != NULL) && (*cors_orig_cfg != 0) && (cors_origin != NULL) && (cors_acrm != NULL)) { /* This is a valid CORS preflight, and the server is configured * to handle it automatically. */ const char *cors_acrh = get_header(ri->http_headers, ri->num_headers, "Access-Control-Request-Headers"); gmt_time_string(date, sizeof(date), &curtime); mg_printf(conn, "HTTP/1.1 200 OK\r\n" "Date: %s\r\n" "Access-Control-Allow-Origin: %s\r\n" "Access-Control-Allow-Methods: %s\r\n" "Content-Length: 0\r\n" "Connection: %s\r\n", date, cors_orig_cfg, ((cors_meth_cfg[0] == '*') ? cors_acrm : cors_meth_cfg), suggest_connection_header(conn)); if (cors_acrh != NULL) { /* CORS request is asking for additional headers */ const char *cors_hdr_cfg = conn->dom_ctx->config[ACCESS_CONTROL_ALLOW_HEADERS]; if ((cors_hdr_cfg != NULL) && (*cors_hdr_cfg != 0)) { /* Allow only if access_control_allow_headers is * not NULL and not an empty string. If this * configuration is set to *, allow everything. * Otherwise this configuration must be a list * of allowed HTTP header names. */ mg_printf(conn, "Access-Control-Allow-Headers: %s\r\n", ((cors_hdr_cfg[0] == '*') ? cors_acrh : cors_hdr_cfg)); } } mg_printf(conn, "Access-Control-Max-Age: 60\r\n"); mg_printf(conn, "\r\n"); DEBUG_TRACE("%s", "OPTIONS done"); return; } } /* 5. interpret the url to find out how the request must be handled */ /* 5.1. first test, if the request targets the regular http(s):// * protocol namespace or the websocket ws(s):// protocol namespace. */ is_websocket_request = (conn->protocol_type == PROTOCOL_TYPE_WEBSOCKET); #if defined(USE_WEBSOCKET) handler_type = is_websocket_request ? WEBSOCKET_HANDLER : REQUEST_HANDLER; #else handler_type = REQUEST_HANDLER; #endif /* defined(USE_WEBSOCKET) */ if (is_websocket_request) { HTTP1_only; } /* 5.2. check if the request will be handled by a callback */ if (get_request_handler(conn, handler_type, &callback_handler, &subprotocols, &ws_connect_handler, &ws_ready_handler, &ws_data_handler, &ws_close_handler, NULL, &callback_data, &handler_info)) { /* 5.2.1. A callback will handle this request. All requests * handled by a callback have to be considered as requests * to a script resource. */ is_callback_resource = 1; is_script_resource = 1; is_put_or_delete_request = is_put_or_delete_method(conn); /* Never handle a C callback according to File WebDav rules, * even if it is a webdav method */ is_webdav_request = 0; /* is_civetweb_webdav_method(conn); */ } else { no_callback_resource: /* 5.2.2. No callback is responsible for this request. The URI * addresses a file based resource (static content or Lua/cgi * scripts in the file system). */ is_callback_resource = 0; interpret_uri(conn, path, sizeof(path), &file.stat, &is_found, &is_script_resource, &is_websocket_request, &is_put_or_delete_request, &is_webdav_request, &is_template_text_file); } /* 5.3. A webdav request (PROPFIND/PROPPATCH/LOCK/UNLOCK) */ if (is_webdav_request) { /* TODO: Do we need a config option? */ const char *webdav_enable = conn->dom_ctx->config[ENABLE_WEBDAV]; if (webdav_enable[0] != 'y') { mg_send_http_error(conn, 405, "%s method not allowed", conn->request_info.request_method); DEBUG_TRACE("%s", "webdav rejected"); return; } } /* 6. authorization check */ /* 6.1. a custom authorization handler is installed */ if (get_request_handler(conn, AUTH_HANDLER, NULL, NULL, NULL, NULL, NULL, NULL, &auth_handler, &auth_callback_data, NULL)) { if (!auth_handler(conn, auth_callback_data)) { /* Callback handler will not be used anymore. Release it */ release_handler_ref(conn, handler_info); DEBUG_TRACE("%s", "auth handler rejected request"); return; } } else if (is_put_or_delete_request && !is_script_resource && !is_callback_resource) { HTTP1_only; /* 6.2. this request is a PUT/DELETE to a real file */ /* 6.2.1. thus, the server must have real files */ #if defined(NO_FILES) if (1) { #else if (conn->dom_ctx->config[DOCUMENT_ROOT] == NULL || conn->dom_ctx->config[PUT_DELETE_PASSWORDS_FILE] == NULL) { #endif /* This code path will not be called for request handlers */ DEBUG_ASSERT(handler_info == NULL); /* This server does not have any real files, thus the * PUT/DELETE methods are not valid. */ mg_send_http_error(conn, 405, "%s method not allowed", conn->request_info.request_method); DEBUG_TRACE("%s", "all file based put/delete requests rejected"); return; } #if !defined(NO_FILES) /* 6.2.2. Check if put authorization for static files is * available. */ if (!is_authorized_for_put(conn)) { send_authorization_request(conn, NULL); DEBUG_TRACE("%s", "file write needs authorization"); return; } #endif } else { /* 6.3. This is either a OPTIONS, GET, HEAD or POST request, * or it is a PUT or DELETE request to a resource that does not * correspond to a file. Check authorization. */ if (!check_authorization(conn, path)) { send_authorization_request(conn, NULL); /* Callback handler will not be used anymore. Release it */ release_handler_ref(conn, handler_info); DEBUG_TRACE("%s", "access authorization required"); return; } } /* request is authorized or does not need authorization */ /* 7. check if there are request handlers for this uri */ if (is_callback_resource) { HTTP1_only; if (!is_websocket_request) { i = callback_handler(conn, callback_data); /* Callback handler will not be used anymore. Release it */ release_handler_ref(conn, handler_info); if (i > 0) { /* Do nothing, callback has served the request. Store * then return value as status code for the log and discard * all data from the client not used by the callback. */ conn->status_code = i; if (!conn->must_close) { discard_unread_request_data(conn); } } else { /* The handler did NOT handle the request. */ /* Some proper reactions would be: * a) close the connections without sending anything * b) send a 404 not found * c) try if there is a file matching the URI * It would be possible to do a, b or c in the callback * implementation, and return 1 - we cannot do anything * here, that is not possible in the callback. * * TODO: What would be the best reaction here? * (Note: The reaction may change, if there is a better * idea.) */ /* For the moment, use option c: We look for a proper file, * but since a file request is not always a script resource, * the authorization check might be different. */ callback_handler = NULL; /* Here we are at a dead end: * According to URI matching, a callback should be * responsible for handling the request, * we called it, but the callback declared itself * not responsible. * We use a goto here, to get out of this dead end, * and continue with the default handling. * A goto here is simpler and better to understand * than some curious loop. */ goto no_callback_resource; } } else { #if defined(USE_WEBSOCKET) handle_websocket_request(conn, path, is_callback_resource, subprotocols, ws_connect_handler, ws_ready_handler, ws_data_handler, ws_close_handler, callback_data); #endif } DEBUG_TRACE("%s", "websocket handling done"); return; } /* 8. handle websocket requests */ #if defined(USE_WEBSOCKET) if (is_websocket_request) { HTTP1_only; if (is_script_resource) { if (is_in_script_path(conn, path)) { /* Websocket Lua script */ handle_websocket_request(conn, path, 0 /* Lua Script */, NULL, NULL, NULL, NULL, NULL, conn->phys_ctx->user_data); } else { /* Script was in an illegal path */ mg_send_http_error(conn, 403, "%s", "Forbidden"); } } else { mg_send_http_error(conn, 404, "%s", "Not found"); } DEBUG_TRACE("%s", "websocket script done"); return; } else #endif #if defined(NO_FILES) /* 9a. In case the server uses only callbacks, this uri is * unknown. * Then, all request handling ends here. */ mg_send_http_error(conn, 404, "%s", "Not Found"); #else /* 9b. This request is either for a static file or resource handled * by a script file. Thus, a DOCUMENT_ROOT must exist. */ if (conn->dom_ctx->config[DOCUMENT_ROOT] == NULL) { mg_send_http_error(conn, 404, "%s", "Not Found"); DEBUG_TRACE("%s", "no document root available"); return; } /* 10. Request is handled by a script */ if (is_script_resource) { HTTP1_only; handle_file_based_request(conn, path, &file); DEBUG_TRACE("%s", "script handling done"); return; } /* Request was not handled by a callback or script. It will be * handled by a server internal method. */ /* 11. Handle put/delete/mkcol requests */ if (is_put_or_delete_request) { HTTP1_only; /* 11.1. PUT method */ if (!strcmp(ri->request_method, "PUT")) { put_file(conn, path); DEBUG_TRACE("handling %s request to %s done", ri->request_method, path); return; } /* 11.2. DELETE method */ if (!strcmp(ri->request_method, "DELETE")) { delete_file(conn, path); DEBUG_TRACE("handling %s request to %s done", ri->request_method, path); return; } /* 11.3. MKCOL method */ if (!strcmp(ri->request_method, "MKCOL")) { dav_mkcol(conn, path); DEBUG_TRACE("handling %s request to %s done", ri->request_method, path); return; } /* 11.4. MOVE method */ if (!strcmp(ri->request_method, "MOVE")) { dav_move_file(conn, path, 0); DEBUG_TRACE("handling %s request to %s done", ri->request_method, path); return; } if (!strcmp(ri->request_method, "COPY")) { dav_move_file(conn, path, 1); DEBUG_TRACE("handling %s request to %s done", ri->request_method, path); return; } /* 11.5. LOCK method */ if (!strcmp(ri->request_method, "LOCK")) { dav_lock_file(conn, path); DEBUG_TRACE("handling %s request to %s done", ri->request_method, path); return; } /* 11.6. UNLOCK method */ if (!strcmp(ri->request_method, "UNLOCK")) { dav_unlock_file(conn, path); DEBUG_TRACE("handling %s request to %s done", ri->request_method, path); return; } /* 11.7. PROPPATCH method */ if (!strcmp(ri->request_method, "PROPPATCH")) { dav_proppatch(conn, path); DEBUG_TRACE("handling %s request to %s done", ri->request_method, path); return; } /* 11.8. Other methods, e.g.: PATCH * This method is not supported for static resources, * only for scripts (Lua, CGI) and callbacks. */ mg_send_http_error(conn, 405, "%s method not allowed", conn->request_info.request_method); DEBUG_TRACE("method %s on %s is not supported", ri->request_method, path); return; } /* 11. File does not exist, or it was configured that it should be * hidden */ if (!is_found || (must_hide_file(conn, path))) { mg_send_http_error(conn, 404, "%s", "Not found"); DEBUG_TRACE("handling %s request to %s: file not found", ri->request_method, path); return; } /* 12. Directory uris should end with a slash */ if (file.stat.is_directory && ((uri_len = (int)strlen(ri->local_uri)) > 0) && (ri->local_uri[uri_len - 1] != '/')) { /* Path + server root */ size_t buflen = UTF8_PATH_MAX * 2 + 2; char *new_path; if (ri->query_string) { buflen += strlen(ri->query_string); } new_path = (char *)mg_malloc_ctx(buflen, conn->phys_ctx); if (!new_path) { mg_send_http_error(conn, 500, "out or memory"); } else { mg_get_request_link(conn, new_path, buflen - 1); strcat(new_path, "/"); if (ri->query_string) { /* Append ? and query string */ strcat(new_path, "?"); strcat(new_path, ri->query_string); } mg_send_http_redirect(conn, new_path, 301); mg_free(new_path); } DEBUG_TRACE("%s request to %s: directory redirection sent", ri->request_method, path); return; } /* 13. Handle other methods than GET/HEAD */ /* 13.1. Handle PROPFIND */ if (!strcmp(ri->request_method, "PROPFIND")) { handle_propfind(conn, path, &file.stat); DEBUG_TRACE("handling %s request to %s done", ri->request_method, path); return; } /* 13.2. Handle OPTIONS for files */ if (!strcmp(ri->request_method, "OPTIONS")) { /* This standard handler is only used for real files. * Scripts should support the OPTIONS method themselves, to allow a * maximum flexibility. * Lua and CGI scripts may fully support CORS this way (including * preflights). */ send_options(conn); DEBUG_TRACE("handling %s request to %s done", ri->request_method, path); return; } /* 13.3. everything but GET and HEAD (e.g. POST) */ if ((0 != strcmp(ri->request_method, "GET")) && (0 != strcmp(ri->request_method, "HEAD"))) { mg_send_http_error(conn, 405, "%s method not allowed", conn->request_info.request_method); DEBUG_TRACE("handling %s request to %s done", ri->request_method, path); return; } /* 14. directories */ if (file.stat.is_directory) { /* Substitute files have already been handled above. */ /* Here we can either generate and send a directory listing, * or send an "access denied" error. */ if (!mg_strcasecmp(conn->dom_ctx->config[ENABLE_DIRECTORY_LISTING], "yes")) { handle_directory_request(conn, path); } else { mg_send_http_error(conn, 403, "%s", "Error: Directory listing denied"); } DEBUG_TRACE("handling %s request to %s done", ri->request_method, path); return; } /* 15. Files with search/replace patterns: LSP and SSI */ if (is_template_text_file) { HTTP1_only; handle_file_based_request(conn, path, &file); DEBUG_TRACE("handling %s request to %s done (template)", ri->request_method, path); return; } /* 16. Static file - maybe cached */ #if !defined(NO_CACHING) if ((!conn->in_error_handler) && is_not_modified(conn, &file.stat)) { /* Send 304 "Not Modified" - this must not send any body data */ handle_not_modified_static_file_request(conn, &file); DEBUG_TRACE("handling %s request to %s done (not modified)", ri->request_method, path); return; } #endif /* !NO_CACHING */ /* 17. Static file - not cached */ handle_static_file_request(conn, path, &file, NULL, NULL); DEBUG_TRACE("handling %s request to %s done (static)", ri->request_method, path); #endif /* !defined(NO_FILES) */ } #if !defined(NO_FILESYSTEMS) static void handle_file_based_request(struct mg_connection *conn, const char *path, struct mg_file *file) { #if !defined(NO_CGI) int cgi_config_idx, inc, max; #endif if (!conn || !conn->dom_ctx) { return; } #if defined(USE_LUA) if (match_prefix_strlen(conn->dom_ctx->config[LUA_SERVER_PAGE_EXTENSIONS], path) > 0) { if (is_in_script_path(conn, path)) { /* Lua server page: an SSI like page containing mostly plain * html code plus some tags with server generated contents. */ handle_lsp_request(conn, path, file, NULL); } else { /* Script was in an illegal path */ mg_send_http_error(conn, 403, "%s", "Forbidden"); } return; } if (match_prefix_strlen(conn->dom_ctx->config[LUA_SCRIPT_EXTENSIONS], path) > 0) { if (is_in_script_path(conn, path)) { /* Lua in-server module script: a CGI like script used to * generate the entire reply. */ mg_exec_lua_script(conn, path, NULL); } else { /* Script was in an illegal path */ mg_send_http_error(conn, 403, "%s", "Forbidden"); } return; } #endif #if defined(USE_DUKTAPE) if (match_prefix_strlen(conn->dom_ctx->config[DUKTAPE_SCRIPT_EXTENSIONS], path) > 0) { if (is_in_script_path(conn, path)) { /* Call duktape to generate the page */ mg_exec_duktape_script(conn, path); } else { /* Script was in an illegal path */ mg_send_http_error(conn, 403, "%s", "Forbidden"); } return; } #endif #if !defined(NO_CGI) inc = CGI2_EXTENSIONS - CGI_EXTENSIONS; max = PUT_DELETE_PASSWORDS_FILE - CGI_EXTENSIONS; for (cgi_config_idx = 0; cgi_config_idx < max; cgi_config_idx += inc) { if (conn->dom_ctx->config[CGI_EXTENSIONS + cgi_config_idx] != NULL) { if (match_prefix_strlen( conn->dom_ctx->config[CGI_EXTENSIONS + cgi_config_idx], path) > 0) { if (is_in_script_path(conn, path)) { /* CGI scripts may support all HTTP methods */ handle_cgi_request(conn, path, cgi_config_idx); } else { /* Script was in an illegal path */ mg_send_http_error(conn, 403, "%s", "Forbidden"); } return; } } } #endif /* !NO_CGI */ if (match_prefix_strlen(conn->dom_ctx->config[SSI_EXTENSIONS], path) > 0) { if (is_in_script_path(conn, path)) { handle_ssi_file_request(conn, path, file); } else { /* Script was in an illegal path */ mg_send_http_error(conn, 403, "%s", "Forbidden"); } return; } #if !defined(NO_CACHING) if ((!conn->in_error_handler) && is_not_modified(conn, &file->stat)) { /* Send 304 "Not Modified" - this must not send any body data */ handle_not_modified_static_file_request(conn, file); return; } #endif /* !NO_CACHING */ handle_static_file_request(conn, path, file, NULL, NULL); } #endif /* NO_FILESYSTEMS */ static void close_all_listening_sockets(struct mg_context *ctx) { unsigned int i; if (!ctx) { return; } for (i = 0; i < ctx->num_listening_sockets; i++) { closesocket(ctx->listening_sockets[i].sock); #if defined(USE_X_DOM_SOCKET) /* For unix domain sockets, the socket name represents a file that has * to be deleted. */ /* See * https://stackoverflow.com/questions/15716302/so-reuseaddr-and-af-unix */ if ((ctx->listening_sockets[i].lsa.sin.sin_family == AF_UNIX) && (ctx->listening_sockets[i].sock != INVALID_SOCKET)) { IGNORE_UNUSED_RESULT( remove(ctx->listening_sockets[i].lsa.sun.sun_path)); } #endif ctx->listening_sockets[i].sock = INVALID_SOCKET; } mg_free(ctx->listening_sockets); ctx->listening_sockets = NULL; mg_free(ctx->listening_socket_fds); ctx->listening_socket_fds = NULL; } /* Valid listening port specification is: [ip_address:]port[s] * Examples for IPv4: 80, 443s, 127.0.0.1:3128, 192.0.2.3:8080s * Examples for IPv6: [::]:80, [::1]:80, * [2001:0db8:7654:3210:FEDC:BA98:7654:3210]:443s * see https://tools.ietf.org/html/rfc3513#section-2.2 * In order to bind to both, IPv4 and IPv6, you can either add * both ports using 8080,[::]:8080, or the short form +8080. * Both forms differ in detail: 8080,[::]:8080 create two sockets, * one only accepting IPv4 the other only IPv6. +8080 creates * one socket accepting IPv4 and IPv6. Depending on the IPv6 * environment, they might work differently, or might not work * at all - it must be tested what options work best in the * relevant network environment. */ static int parse_port_string(const struct vec *vec, struct socket *so, int *ip_version) { unsigned int a, b, c, d; unsigned port; unsigned long portUL; int ch, len; const char *cb; char *endptr; #if defined(USE_IPV6) char buf[100] = {0}; #endif /* MacOS needs that. If we do not zero it, subsequent bind() will fail. * Also, all-zeroes in the socket address means binding to all addresses * for both IPv4 and IPv6 (INADDR_ANY and IN6ADDR_ANY_INIT). */ memset(so, 0, sizeof(*so)); so->lsa.sin.sin_family = AF_INET; *ip_version = 0; /* Initialize len as invalid. */ port = 0; len = 0; /* Test for different ways to format this string */ if (sscanf(vec->ptr, "%u.%u.%u.%u:%u%n", &a, &b, &c, &d, &port, &len) // NOLINT(cert-err34-c) 'sscanf' used to convert a string // to an integer value, but function will not report // conversion errors; consider using 'strtol' instead == 5) { /* Bind to a specific IPv4 address, e.g. 192.168.1.5:8080 */ so->lsa.sin.sin_addr.s_addr = htonl((a << 24) | (b << 16) | (c << 8) | d); so->lsa.sin.sin_port = htons((uint16_t)port); *ip_version = 4; #if defined(USE_IPV6) } else if (sscanf(vec->ptr, "[%49[^]]]:%u%n", buf, &port, &len) == 2 && ((size_t)len <= vec->len) && mg_inet_pton( AF_INET6, buf, &so->lsa.sin6, sizeof(so->lsa.sin6), 0)) { /* IPv6 address, examples: see above */ /* so->lsa.sin6.sin6_family = AF_INET6; already set by mg_inet_pton */ so->lsa.sin6.sin6_port = htons((uint16_t)port); *ip_version = 6; #endif } else if ((vec->ptr[0] == '+') && (sscanf(vec->ptr + 1, "%u%n", &port, &len) == 1)) { // NOLINT(cert-err34-c) 'sscanf' used to convert a // string to an integer value, but function will not // report conversion errors; consider using 'strtol' // instead /* Port is specified with a +, bind to IPv6 and IPv4, INADDR_ANY */ /* Add 1 to len for the + character we skipped before */ len++; #if defined(USE_IPV6) /* Set socket family to IPv6, do not use IPV6_V6ONLY */ so->lsa.sin6.sin6_family = AF_INET6; so->lsa.sin6.sin6_port = htons((uint16_t)port); *ip_version = 4 + 6; #else /* Bind to IPv4 only, since IPv6 is not built in. */ so->lsa.sin.sin_port = htons((uint16_t)port); *ip_version = 4; #endif } else if (is_valid_port(portUL = strtoul(vec->ptr, &endptr, 0)) && (vec->ptr != endptr)) { len = (int)(endptr - vec->ptr); port = (uint16_t)portUL; /* If only port is specified, bind to IPv4, INADDR_ANY */ so->lsa.sin.sin_port = htons((uint16_t)port); *ip_version = 4; } else if ((cb = strchr(vec->ptr, ':')) != NULL) { /* String could be a hostname. This check algorithm * will only work for RFC 952 compliant hostnames, * starting with a letter, containing only letters, * digits and hyphen ('-'). Newer specs may allow * more, but this is not guaranteed here, since it * may interfere with rules for port option lists. */ /* According to RFC 1035, hostnames are restricted to 255 characters * in total (63 between two dots). */ char hostname[256]; size_t hostnlen = (size_t)(cb - vec->ptr); if ((hostnlen >= vec->len) || (hostnlen >= sizeof(hostname))) { /* This would be invalid in any case */ *ip_version = 0; return 0; } mg_strlcpy(hostname, vec->ptr, hostnlen + 1); if (mg_inet_pton( AF_INET, hostname, &so->lsa.sin, sizeof(so->lsa.sin), 1)) { if (sscanf(cb + 1, "%u%n", &port, &len) == 1) { // NOLINT(cert-err34-c) 'sscanf' used to convert a // string to an integer value, but function will not // report conversion errors; consider using 'strtol' // instead *ip_version = 4; so->lsa.sin.sin_port = htons((uint16_t)port); len += (int)(hostnlen + 1); } else { len = 0; } #if defined(USE_IPV6) } else if (mg_inet_pton(AF_INET6, hostname, &so->lsa.sin6, sizeof(so->lsa.sin6), 1)) { if (sscanf(cb + 1, "%u%n", &port, &len) == 1) { *ip_version = 6; so->lsa.sin6.sin6_port = htons((uint16_t)port); len += (int)(hostnlen + 1); } else { len = 0; } #endif } else { len = 0; } #if defined(USE_X_DOM_SOCKET) } else if (vec->ptr[0] == 'x') { /* unix (linux) domain socket */ if (vec->len < sizeof(so->lsa.sun.sun_path)) { len = vec->len; so->lsa.sun.sun_family = AF_UNIX; memset(so->lsa.sun.sun_path, 0, sizeof(so->lsa.sun.sun_path)); memcpy(so->lsa.sun.sun_path, (char *)vec->ptr + 1, vec->len - 1); port = 0; *ip_version = 99; } else { /* String too long */ len = 0; } #endif } else { /* Parsing failure. */ len = 0; } /* sscanf and the option splitting code ensure the following condition * Make sure the port is valid and vector ends with the port, 's' or 'r' */ if ((len > 0) && is_valid_port(port) && (((size_t)len == vec->len) || (((size_t)len + 1) == vec->len))) { /* Next character after the port number */ ch = ((size_t)len < vec->len) ? vec->ptr[len] : '\0'; so->is_ssl = (ch == 's'); so->ssl_redir = (ch == 'r'); if ((ch == '\0') || (ch == 's') || (ch == 'r')) { return 1; } } /* Reset ip_version to 0 if there is an error */ *ip_version = 0; return 0; } /* Is there any SSL port in use? */ static int is_ssl_port_used(const char *ports) { if (ports) { /* There are several different allowed syntax variants: * - "80" for a single port using every network interface * - "localhost:80" for a single port using only localhost * - "80,localhost:8080" for two ports, one bound to localhost * - "80,127.0.0.1:8084,[::1]:8086" for three ports, one bound * to IPv4 localhost, one to IPv6 localhost * - "+80" use port 80 for IPv4 and IPv6 * - "+80r,+443s" port 80 (HTTP) is a redirect to port 443 (HTTPS), * for both: IPv4 and IPv4 * - "+443s,localhost:8080" port 443 (HTTPS) for every interface, * additionally port 8080 bound to localhost connections * * If we just look for 's' anywhere in the string, "localhost:80" * will be detected as SSL (false positive). * Looking for 's' after a digit may cause false positives in * "my24service:8080". * Looking from 's' backward if there are only ':' and numbers * before will not work for "24service:8080" (non SSL, port 8080) * or "24s" (SSL, port 24). * * Remark: Initially hostnames were not allowed to start with a * digit (according to RFC 952), this was allowed later (RFC 1123, * Section 2.1). * * To get this correct, the entire string must be parsed as a whole, * reading it as a list element for element and parsing with an * algorithm equivalent to parse_port_string. * * In fact, we use local interface names here, not arbitrary * hostnames, so in most cases the only name will be "localhost". * * So, for now, we use this simple algorithm, that may still return * a false positive in bizarre cases. */ int i; int portslen = (int)strlen(ports); char prevIsNumber = 0; for (i = 0; i < portslen; i++) { if (prevIsNumber && (ports[i] == 's' || ports[i] == 'r')) { return 1; } if (ports[i] >= '0' && ports[i] <= '9') { prevIsNumber = 1; } else { prevIsNumber = 0; } } } return 0; } static int set_ports_option(struct mg_context *phys_ctx) { const char *list; int on = 1; #if defined(USE_IPV6) int off = 0; #endif struct vec vec; struct socket so, *ptr; struct mg_pollfd *pfd; union usa usa; socklen_t len; int ip_version; int portsTotal = 0; int portsOk = 0; const char *opt_txt; long opt_listen_backlog; if (!phys_ctx) { return 0; } memset(&so, 0, sizeof(so)); memset(&usa, 0, sizeof(usa)); len = sizeof(usa); list = phys_ctx->dd.config[LISTENING_PORTS]; while ((list = next_option(list, &vec, NULL)) != NULL) { portsTotal++; if (!parse_port_string(&vec, &so, &ip_version)) { mg_cry_ctx_internal( phys_ctx, "%.*s: invalid port spec (entry %i). Expecting list of: %s", (int)vec.len, vec.ptr, portsTotal, "[IP_ADDRESS:]PORT[s|r]"); continue; } #if !defined(NO_SSL) if (so.is_ssl && phys_ctx->dd.ssl_ctx == NULL) { mg_cry_ctx_internal(phys_ctx, "Cannot add SSL socket (entry %i)", portsTotal); continue; } #endif /* Create socket. */ /* For a list of protocol numbers (e.g., TCP==6) see: * https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml */ if ((so.sock = socket(so.lsa.sa.sa_family, SOCK_STREAM, (ip_version == 99) ? (/* LOCAL */ 0) : (/* TCP */ 6))) == INVALID_SOCKET) { mg_cry_ctx_internal(phys_ctx, "cannot create socket (entry %i)", portsTotal); continue; } #if defined(_WIN32) /* Windows SO_REUSEADDR lets many procs binds to a * socket, SO_EXCLUSIVEADDRUSE makes the bind fail * if someone already has the socket -- DTL */ /* NOTE: If SO_EXCLUSIVEADDRUSE is used, * Windows might need a few seconds before * the same port can be used again in the * same process, so a short Sleep may be * required between mg_stop and mg_start. */ if (setsockopt(so.sock, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, (SOCK_OPT_TYPE)&on, sizeof(on)) != 0) { /* Set reuse option, but don't abort on errors. */ mg_cry_ctx_internal( phys_ctx, "cannot set socket option SO_EXCLUSIVEADDRUSE (entry %i)", portsTotal); } #else if (setsockopt(so.sock, SOL_SOCKET, SO_REUSEADDR, (SOCK_OPT_TYPE)&on, sizeof(on)) != 0) { /* Set reuse option, but don't abort on errors. */ mg_cry_ctx_internal( phys_ctx, "cannot set socket option SO_REUSEADDR (entry %i)", portsTotal); } #endif #if defined(USE_X_DOM_SOCKET) if (ip_version == 99) { /* Unix domain socket */ } else #endif if (ip_version > 4) { /* Could be 6 for IPv6 onlyor 10 (4+6) for IPv4+IPv6 */ #if defined(USE_IPV6) if (ip_version > 6) { if (so.lsa.sa.sa_family == AF_INET6 && setsockopt(so.sock, IPPROTO_IPV6, IPV6_V6ONLY, (void *)&off, sizeof(off)) != 0) { /* Set IPv6 only option, but don't abort on errors. */ mg_cry_ctx_internal(phys_ctx, "cannot set socket option " "IPV6_V6ONLY=off (entry %i)", portsTotal); } } else { if (so.lsa.sa.sa_family == AF_INET6 && setsockopt(so.sock, IPPROTO_IPV6, IPV6_V6ONLY, (void *)&on, sizeof(on)) != 0) { /* Set IPv6 only option, but don't abort on errors. */ mg_cry_ctx_internal(phys_ctx, "cannot set socket option " "IPV6_V6ONLY=on (entry %i)", portsTotal); } } #else mg_cry_ctx_internal(phys_ctx, "%s", "IPv6 not available"); closesocket(so.sock); so.sock = INVALID_SOCKET; continue; #endif } if (so.lsa.sa.sa_family == AF_INET) { len = sizeof(so.lsa.sin); if (bind(so.sock, &so.lsa.sa, len) != 0) { mg_cry_ctx_internal(phys_ctx, "cannot bind to %.*s: %d (%s)", (int)vec.len, vec.ptr, (int)ERRNO, strerror(errno)); closesocket(so.sock); so.sock = INVALID_SOCKET; continue; } } #if defined(USE_IPV6) else if (so.lsa.sa.sa_family == AF_INET6) { len = sizeof(so.lsa.sin6); if (bind(so.sock, &so.lsa.sa, len) != 0) { mg_cry_ctx_internal(phys_ctx, "cannot bind to IPv6 %.*s: %d (%s)", (int)vec.len, vec.ptr, (int)ERRNO, strerror(errno)); closesocket(so.sock); so.sock = INVALID_SOCKET; continue; } } #endif #if defined(USE_X_DOM_SOCKET) else if (so.lsa.sa.sa_family == AF_UNIX) { len = sizeof(so.lsa.sun); if (bind(so.sock, &so.lsa.sa, len) != 0) { mg_cry_ctx_internal(phys_ctx, "cannot bind to unix socket %s: %d (%s)", so.lsa.sun.sun_path, (int)ERRNO, strerror(errno)); closesocket(so.sock); so.sock = INVALID_SOCKET; continue; } } #endif else { mg_cry_ctx_internal( phys_ctx, "cannot bind: address family not supported (entry %i)", portsTotal); closesocket(so.sock); so.sock = INVALID_SOCKET; continue; } opt_txt = phys_ctx->dd.config[LISTEN_BACKLOG_SIZE]; opt_listen_backlog = strtol(opt_txt, NULL, 10); if ((opt_listen_backlog > INT_MAX) || (opt_listen_backlog < 1)) { mg_cry_ctx_internal(phys_ctx, "%s value \"%s\" is invalid", config_options[LISTEN_BACKLOG_SIZE].name, opt_txt); closesocket(so.sock); so.sock = INVALID_SOCKET; continue; } if (listen(so.sock, (int)opt_listen_backlog) != 0) { mg_cry_ctx_internal(phys_ctx, "cannot listen to %.*s: %d (%s)", (int)vec.len, vec.ptr, (int)ERRNO, strerror(errno)); closesocket(so.sock); so.sock = INVALID_SOCKET; continue; } if ((getsockname(so.sock, &(usa.sa), &len) != 0) || (usa.sa.sa_family != so.lsa.sa.sa_family)) { int err = (int)ERRNO; mg_cry_ctx_internal(phys_ctx, "call to getsockname failed %.*s: %d (%s)", (int)vec.len, vec.ptr, err, strerror(errno)); closesocket(so.sock); so.sock = INVALID_SOCKET; continue; } /* Update lsa port in case of random free ports */ #if defined(USE_IPV6) if (so.lsa.sa.sa_family == AF_INET6) { so.lsa.sin6.sin6_port = usa.sin6.sin6_port; } else #endif { so.lsa.sin.sin_port = usa.sin.sin_port; } if ((ptr = (struct socket *) mg_realloc_ctx(phys_ctx->listening_sockets, (phys_ctx->num_listening_sockets + 1) * sizeof(phys_ctx->listening_sockets[0]), phys_ctx)) == NULL) { mg_cry_ctx_internal(phys_ctx, "%s", "Out of memory"); closesocket(so.sock); so.sock = INVALID_SOCKET; continue; } if ((pfd = (struct mg_pollfd *) mg_realloc_ctx(phys_ctx->listening_socket_fds, (phys_ctx->num_listening_sockets + 1) * sizeof(phys_ctx->listening_socket_fds[0]), phys_ctx)) == NULL) { mg_cry_ctx_internal(phys_ctx, "%s", "Out of memory"); closesocket(so.sock); so.sock = INVALID_SOCKET; mg_free(ptr); continue; } set_close_on_exec(so.sock, NULL, phys_ctx); phys_ctx->listening_sockets = ptr; phys_ctx->listening_sockets[phys_ctx->num_listening_sockets] = so; phys_ctx->listening_socket_fds = pfd; phys_ctx->num_listening_sockets++; portsOk++; } if (portsOk != portsTotal) { close_all_listening_sockets(phys_ctx); portsOk = 0; } return portsOk; } static const char * header_val(const struct mg_connection *conn, const char *header) { const char *header_value; if ((header_value = mg_get_header(conn, header)) == NULL) { return "-"; } else { return header_value; } } #if defined(MG_EXTERNAL_FUNCTION_log_access) #include "external_log_access.h" #elif !defined(NO_FILESYSTEMS) static void log_access(const struct mg_connection *conn) { const struct mg_request_info *ri; struct mg_file fi; char date[64], src_addr[IP_ADDR_STR_LEN]; #if defined(REENTRANT_TIME) struct tm _tm; struct tm *tm = &_tm; #else struct tm *tm; #endif const char *referer; const char *user_agent; char log_buf[4096]; if (!conn || !conn->dom_ctx) { return; } /* Set log message to "empty" */ log_buf[0] = 0; #if defined(USE_LUA) if (conn->phys_ctx->lua_bg_log_available) { int ret; struct mg_context *ctx = conn->phys_ctx; lua_State *lstate = (lua_State *)ctx->lua_background_state; pthread_mutex_lock(&ctx->lua_bg_mutex); /* call "log()" in Lua */ lua_getglobal(lstate, "log"); prepare_lua_request_info_inner(conn, lstate); push_lua_response_log_data(conn, lstate); ret = lua_pcall(lstate, /* args */ 2, /* results */ 1, 0); if (ret == 0) { int t = lua_type(lstate, -1); if (t == LUA_TBOOLEAN) { if (lua_toboolean(lstate, -1) == 0) { /* log() returned false: do not log */ pthread_mutex_unlock(&ctx->lua_bg_mutex); return; } /* log returned true: continue logging */ } else if (t == LUA_TSTRING) { size_t len; const char *txt = lua_tolstring(lstate, -1, &len); if ((len == 0) || (*txt == 0)) { /* log() returned empty string: do not log */ pthread_mutex_unlock(&ctx->lua_bg_mutex); return; } /* Copy test from Lua into log_buf */ if (len >= sizeof(log_buf)) { len = sizeof(log_buf) - 1; } memcpy(log_buf, txt, len); log_buf[len] = 0; } } else { lua_cry(conn, ret, lstate, "lua_background_script", "log"); } pthread_mutex_unlock(&ctx->lua_bg_mutex); } #endif if (conn->dom_ctx->config[ACCESS_LOG_FILE] != NULL) { if (mg_fopen(conn, conn->dom_ctx->config[ACCESS_LOG_FILE], MG_FOPEN_MODE_APPEND, &fi) == 0) { fi.access.fp = NULL; } } else { fi.access.fp = NULL; } /* Log is written to a file and/or a callback. If both are not set, * executing the rest of the function is pointless. */ if ((fi.access.fp == NULL) && (conn->phys_ctx->callbacks.log_access == NULL)) { return; } /* If we did not get a log message from Lua, create it here. */ if (!log_buf[0]) { #if defined(REENTRANT_TIME) localtime_r(&conn->conn_birth_time, tm); #else tm = localtime(&conn->conn_birth_time); #endif if (tm != NULL) { strftime(date, sizeof(date), "%d/%b/%Y:%H:%M:%S %z", tm); } else { mg_strlcpy(date, "01/Jan/1970:00:00:00 +0000", sizeof(date)); } ri = &conn->request_info; sockaddr_to_string(src_addr, sizeof(src_addr), &conn->client.rsa); referer = header_val(conn, "Referer"); user_agent = header_val(conn, "User-Agent"); mg_snprintf(conn, NULL, /* Ignore truncation in access log */ log_buf, sizeof(log_buf), "%s - %s [%s] \"%s %s%s%s HTTP/%s\" %d %" INT64_FMT " %s %s", src_addr, (ri->remote_user == NULL) ? "-" : ri->remote_user, date, ri->request_method ? ri->request_method : "-", ri->request_uri ? ri->request_uri : "-", ri->query_string ? "?" : "", ri->query_string ? ri->query_string : "", ri->http_version, conn->status_code, conn->num_bytes_sent, referer, user_agent); } /* Here we have a log message in log_buf. Call the callback */ if (conn->phys_ctx->callbacks.log_access) { if (conn->phys_ctx->callbacks.log_access(conn, log_buf)) { /* do not log if callback returns non-zero */ if (fi.access.fp) { mg_fclose(&fi.access); } return; } } /* Store in file */ if (fi.access.fp) { int ok = 1; flockfile(fi.access.fp); if (fprintf(fi.access.fp, "%s\n", log_buf) < 1) { ok = 0; } if (fflush(fi.access.fp) != 0) { ok = 0; } funlockfile(fi.access.fp); if (mg_fclose(&fi.access) != 0) { ok = 0; } if (!ok) { mg_cry_internal(conn, "Error writing log file %s", conn->dom_ctx->config[ACCESS_LOG_FILE]); } } } #else #error "Either enable filesystems or provide a custom log_access implementation" #endif /* Externally provided function */ /* Verify given socket address against the ACL. * Return -1 if ACL is malformed, 0 if address is disallowed, 1 if allowed. */ static int check_acl(struct mg_context *phys_ctx, const union usa *sa) { int allowed, flag, matched; struct vec vec; if (phys_ctx) { const char *list = phys_ctx->dd.config[ACCESS_CONTROL_LIST]; /* If any ACL is set, deny by default */ allowed = (list == NULL) ? '+' : '-'; while ((list = next_option(list, &vec, NULL)) != NULL) { flag = vec.ptr[0]; matched = -1; if ((vec.len > 0) && ((flag == '+') || (flag == '-'))) { vec.ptr++; vec.len--; matched = parse_match_net(&vec, sa, 1); } if (matched < 0) { mg_cry_ctx_internal(phys_ctx, "%s: subnet must be [+|-]IP-addr[/x]", __func__); return -1; } if (matched) { allowed = flag; } } return allowed == '+'; } return -1; } #if !defined(_WIN32) && !defined(__ZEPHYR__) static int set_uid_option(struct mg_context *phys_ctx) { int success = 0; if (phys_ctx) { /* We are currently running as curr_uid. */ const uid_t curr_uid = getuid(); /* If set, we want to run as run_as_user. */ const char *run_as_user = phys_ctx->dd.config[RUN_AS_USER]; const struct passwd *to_pw = NULL; if ((run_as_user != NULL) && (to_pw = getpwnam(run_as_user)) == NULL) { /* run_as_user does not exist on the system. We can't proceed * further. */ mg_cry_ctx_internal(phys_ctx, "%s: unknown user [%s]", __func__, run_as_user); } else if ((run_as_user == NULL) || (curr_uid == to_pw->pw_uid)) { /* There was either no request to change user, or we're already * running as run_as_user. Nothing else to do. */ success = 1; } else { /* Valid change request. */ if (setgid(to_pw->pw_gid) == -1) { mg_cry_ctx_internal(phys_ctx, "%s: setgid(%s): %s", __func__, run_as_user, strerror(errno)); } else if (setgroups(0, NULL) == -1) { mg_cry_ctx_internal(phys_ctx, "%s: setgroups(): %s", __func__, strerror(errno)); } else if (setuid(to_pw->pw_uid) == -1) { mg_cry_ctx_internal(phys_ctx, "%s: setuid(%s): %s", __func__, run_as_user, strerror(errno)); } else { success = 1; } } } return success; } #endif /* !_WIN32 */ static void tls_dtor(void *key) { struct mg_workerTLS *tls = (struct mg_workerTLS *)key; /* key == pthread_getspecific(sTlsKey); */ if (tls) { if (tls->is_master == 2) { tls->is_master = -3; /* Mark memory as dead */ mg_free(tls); } } pthread_setspecific(sTlsKey, NULL); } #if defined(USE_MBEDTLS) /* Check if SSL is required. * If so, set up ctx->ssl_ctx pointer. */ static int mg_sslctx_init(struct mg_context *phys_ctx, struct mg_domain_context *dom_ctx) { if (!phys_ctx) { return 0; } if (!dom_ctx) { dom_ctx = &(phys_ctx->dd); } if (!is_ssl_port_used(dom_ctx->config[LISTENING_PORTS])) { /* No SSL port is set. No need to setup SSL. */ return 1; } dom_ctx->ssl_ctx = (SSL_CTX *)mg_calloc(1, sizeof(*dom_ctx->ssl_ctx)); if (dom_ctx->ssl_ctx == NULL) { fprintf(stderr, "ssl_ctx malloc failed\n"); return 0; } return mbed_sslctx_init(dom_ctx->ssl_ctx, dom_ctx->config[SSL_CERTIFICATE]) == 0 ? 1 : 0; } #elif !defined(NO_SSL) static int ssl_use_pem_file(struct mg_context *phys_ctx, struct mg_domain_context *dom_ctx, const char *pem, const char *chain); static const char *ssl_error(void); static int refresh_trust(struct mg_connection *conn) { struct stat cert_buf; int64_t t = 0; const char *pem; const char *chain; int should_verify_peer; if ((pem = conn->dom_ctx->config[SSL_CERTIFICATE]) == NULL) { /* If pem is NULL and conn->phys_ctx->callbacks.init_ssl is not, * refresh_trust still can not work. */ return 0; } chain = conn->dom_ctx->config[SSL_CERTIFICATE_CHAIN]; if (chain == NULL) { /* pem is not NULL here */ chain = pem; } if (*chain == 0) { chain = NULL; } if (stat(pem, &cert_buf) != -1) { t = (int64_t)cert_buf.st_mtime; } mg_lock_context(conn->phys_ctx); if ((t != 0) && (conn->dom_ctx->ssl_cert_last_mtime != t)) { conn->dom_ctx->ssl_cert_last_mtime = t; should_verify_peer = 0; if (conn->dom_ctx->config[SSL_DO_VERIFY_PEER] != NULL) { if (mg_strcasecmp(conn->dom_ctx->config[SSL_DO_VERIFY_PEER], "yes") == 0) { should_verify_peer = 1; } else if (mg_strcasecmp(conn->dom_ctx->config[SSL_DO_VERIFY_PEER], "optional") == 0) { should_verify_peer = 1; } } if (should_verify_peer) { char *ca_path = conn->dom_ctx->config[SSL_CA_PATH]; char *ca_file = conn->dom_ctx->config[SSL_CA_FILE]; if (SSL_CTX_load_verify_locations(conn->dom_ctx->ssl_ctx, ca_file, ca_path) != 1) { mg_unlock_context(conn->phys_ctx); mg_cry_ctx_internal( conn->phys_ctx, "SSL_CTX_load_verify_locations error: %s " "ssl_verify_peer requires setting " "either ssl_ca_path or ssl_ca_file. Is any of them " "present in " "the .conf file?", ssl_error()); return 0; } } if (ssl_use_pem_file(conn->phys_ctx, conn->dom_ctx, pem, chain) == 0) { mg_unlock_context(conn->phys_ctx); return 0; } } mg_unlock_context(conn->phys_ctx); return 1; } #if defined(OPENSSL_API_1_1) #else static pthread_mutex_t *ssl_mutexes; #endif /* OPENSSL_API_1_1 */ static int sslize(struct mg_connection *conn, int (*func)(SSL *), const struct mg_client_options *client_options) { int ret, err; int short_trust; unsigned timeout = 1024; unsigned i; if (!conn) { return 0; } short_trust = (conn->dom_ctx->config[SSL_SHORT_TRUST] != NULL) && (mg_strcasecmp(conn->dom_ctx->config[SSL_SHORT_TRUST], "yes") == 0); if (short_trust) { int trust_ret = refresh_trust(conn); if (!trust_ret) { return trust_ret; } } mg_lock_context(conn->phys_ctx); conn->ssl = SSL_new(conn->dom_ctx->ssl_ctx); mg_unlock_context(conn->phys_ctx); if (conn->ssl == NULL) { mg_cry_internal(conn, "sslize error: %s", ssl_error()); OPENSSL_REMOVE_THREAD_STATE(); return 0; } SSL_set_app_data(conn->ssl, (char *)conn); ret = SSL_set_fd(conn->ssl, conn->client.sock); if (ret != 1) { mg_cry_internal(conn, "sslize error: %s", ssl_error()); SSL_free(conn->ssl); conn->ssl = NULL; OPENSSL_REMOVE_THREAD_STATE(); return 0; } if (client_options) { if (client_options->host_name) { SSL_set_tlsext_host_name(conn->ssl, client_options->host_name); } } /* Reuse the request timeout for the SSL_Accept/SSL_connect timeout */ if (conn->dom_ctx->config[REQUEST_TIMEOUT]) { /* NOTE: The loop below acts as a back-off, so we can end * up sleeping for more (or less) than the REQUEST_TIMEOUT. */ int to = atoi(conn->dom_ctx->config[REQUEST_TIMEOUT]); if (to >= 0) { timeout = (unsigned)to; } } /* SSL functions may fail and require to be called again: * see https://www.openssl.org/docs/manmaster/ssl/SSL_get_error.html * Here "func" could be SSL_connect or SSL_accept. */ for (i = 0; i <= timeout; i += 50) { ERR_clear_error(); /* conn->dom_ctx may be changed here (see ssl_servername_callback) */ ret = func(conn->ssl); if (ret != 1) { err = SSL_get_error(conn->ssl, ret); if ((err == SSL_ERROR_WANT_CONNECT) || (err == SSL_ERROR_WANT_ACCEPT) || (err == SSL_ERROR_WANT_READ) || (err == SSL_ERROR_WANT_WRITE) || (err == SSL_ERROR_WANT_X509_LOOKUP)) { if (!STOP_FLAG_IS_ZERO(&conn->phys_ctx->stop_flag)) { /* Don't wait if the server is going to be stopped. */ break; } if (err == SSL_ERROR_WANT_X509_LOOKUP) { /* Simply retry the function call. */ mg_sleep(50); } else { /* Need to retry the function call "later". * See https://linux.die.net/man/3/ssl_get_error * This is typical for non-blocking sockets. */ struct mg_pollfd pfd; int pollres; pfd.fd = conn->client.sock; pfd.events = ((err == SSL_ERROR_WANT_CONNECT) || (err == SSL_ERROR_WANT_WRITE)) ? POLLOUT : POLLIN; pollres = mg_poll(&pfd, 1, 50, &(conn->phys_ctx->stop_flag)); if (pollres < 0) { /* Break if error occurred (-1) * or server shutdown (-2) */ break; } } } else if (err == SSL_ERROR_SYSCALL) { /* This is an IO error. Look at errno. */ mg_cry_internal(conn, "SSL syscall error %i", ERRNO); break; } else { /* This is an SSL specific error, e.g. SSL_ERROR_SSL */ mg_cry_internal(conn, "sslize error: %s", ssl_error()); break; } } else { /* success */ break; } } ERR_clear_error(); if (ret != 1) { SSL_free(conn->ssl); conn->ssl = NULL; OPENSSL_REMOVE_THREAD_STATE(); return 0; } return 1; } /* Return OpenSSL error message (from CRYPTO lib) */ static const char * ssl_error(void) { unsigned long err; err = ERR_get_error(); return ((err == 0) ? "" : ERR_error_string(err, NULL)); } static int hexdump2string(void *mem, int memlen, char *buf, int buflen) { int i; const char hexdigit[] = "0123456789abcdef"; if ((memlen <= 0) || (buflen <= 0)) { return 0; } if (buflen < (3 * memlen)) { return 0; } for (i = 0; i < memlen; i++) { if (i > 0) { buf[3 * i - 1] = ' '; } buf[3 * i] = hexdigit[(((uint8_t *)mem)[i] >> 4) & 0xF]; buf[3 * i + 1] = hexdigit[((uint8_t *)mem)[i] & 0xF]; } buf[3 * memlen - 1] = 0; return 1; } static int ssl_get_client_cert_info(const struct mg_connection *conn, struct mg_client_cert *client_cert) { X509 *cert = SSL_get_peer_certificate(conn->ssl); if (cert) { char str_buf[1024]; unsigned char buf[256]; char *str_serial = NULL; unsigned int ulen; int ilen; unsigned char *tmp_buf; unsigned char *tmp_p; /* Handle to algorithm used for fingerprint */ const EVP_MD *digest = EVP_get_digestbyname("sha1"); /* Get Subject and issuer */ X509_NAME *subj = X509_get_subject_name(cert); X509_NAME *iss = X509_get_issuer_name(cert); /* Get serial number */ ASN1_INTEGER *serial = X509_get_serialNumber(cert); /* Translate serial number to a hex string */ BIGNUM *serial_bn = ASN1_INTEGER_to_BN(serial, NULL); if (serial_bn) { str_serial = BN_bn2hex(serial_bn); BN_free(serial_bn); } client_cert->serial = str_serial ? mg_strdup_ctx(str_serial, conn->phys_ctx) : NULL; /* Translate subject and issuer to a string */ (void)X509_NAME_oneline(subj, str_buf, (int)sizeof(str_buf)); client_cert->subject = mg_strdup_ctx(str_buf, conn->phys_ctx); (void)X509_NAME_oneline(iss, str_buf, (int)sizeof(str_buf)); client_cert->issuer = mg_strdup_ctx(str_buf, conn->phys_ctx); /* Calculate SHA1 fingerprint and store as a hex string */ ulen = 0; /* ASN1_digest is deprecated. Do the calculation manually, * using EVP_Digest. */ ilen = i2d_X509(cert, NULL); tmp_buf = (ilen > 0) ? (unsigned char *)mg_malloc_ctx((unsigned)ilen + 1, conn->phys_ctx) : NULL; if (tmp_buf) { tmp_p = tmp_buf; (void)i2d_X509(cert, &tmp_p); if (!EVP_Digest( tmp_buf, (unsigned)ilen, buf, &ulen, digest, NULL)) { ulen = 0; } mg_free(tmp_buf); } if (!hexdump2string(buf, (int)ulen, str_buf, (int)sizeof(str_buf))) { *str_buf = 0; } client_cert->finger = mg_strdup_ctx(str_buf, conn->phys_ctx); client_cert->peer_cert = (void *)cert; /* Strings returned from bn_bn2hex must be freed using OPENSSL_free, * see https://linux.die.net/man/3/bn_bn2hex */ OPENSSL_free(str_serial); return 1; } return 0; } #if defined(OPENSSL_API_1_1) #else static void ssl_locking_callback(int mode, int mutex_num, const char *file, int line) { (void)line; (void)file; if (mode & 1) { /* 1 is CRYPTO_LOCK */ (void)pthread_mutex_lock(&ssl_mutexes[mutex_num]); } else { (void)pthread_mutex_unlock(&ssl_mutexes[mutex_num]); } } #endif /* OPENSSL_API_1_1 */ #if !defined(NO_SSL_DL) /* Load a DLL/Shared Object with a TLS/SSL implementation. */ static void * load_tls_dll(char *ebuf, size_t ebuf_len, const char *dll_name, struct ssl_func *sw, int *feature_missing) { union { void *p; void (*fp)(void); } u; void *dll_handle; struct ssl_func *fp; int ok; int truncated = 0; if ((dll_handle = dlopen(dll_name, RTLD_LAZY)) == NULL) { mg_snprintf(NULL, NULL, /* No truncation check for ebuf */ ebuf, ebuf_len, "%s: cannot load %s", __func__, dll_name); return NULL; } ok = 1; for (fp = sw; fp->name != NULL; fp++) { #if defined(_WIN32) /* GetProcAddress() returns pointer to function */ u.fp = (void (*)(void))dlsym(dll_handle, fp->name); #else /* dlsym() on UNIX returns void *. ISO C forbids casts of data * pointers to function pointers. We need to use a union to make a * cast. */ u.p = dlsym(dll_handle, fp->name); #endif /* _WIN32 */ /* Set pointer (might be NULL) */ fp->ptr = u.fp; if (u.fp == NULL) { DEBUG_TRACE("Missing function: %s\n", fp->name); if (feature_missing) { feature_missing[fp->required]++; } if (fp->required == TLS_Mandatory) { /* Mandatory function is missing */ if (ok) { /* This is the first missing function. * Create a new error message. */ mg_snprintf(NULL, &truncated, ebuf, ebuf_len, "%s: %s: cannot find %s", __func__, dll_name, fp->name); ok = 0; } else { /* This is yet anothermissing function. * Append existing error message. */ size_t cur_len = strlen(ebuf); if (!truncated && ((ebuf_len - cur_len) > 3)) { mg_snprintf(NULL, &truncated, ebuf + cur_len, ebuf_len - cur_len - 3, ", %s", fp->name); if (truncated) { /* If truncated, add "..." */ strcat(ebuf, "..."); } } } } } } if (!ok) { (void)dlclose(dll_handle); return NULL; } return dll_handle; } static void *ssllib_dll_handle; /* Store the ssl library handle. */ static void *cryptolib_dll_handle; /* Store the crypto library handle. */ #endif /* NO_SSL_DL */ #if defined(SSL_ALREADY_INITIALIZED) static volatile ptrdiff_t cryptolib_users = 1; /* Reference counter for crypto library. */ #else static volatile ptrdiff_t cryptolib_users = 0; /* Reference counter for crypto library. */ #endif static int initialize_openssl(char *ebuf, size_t ebuf_len) { #if !defined(OPENSSL_API_1_1) && !defined(OPENSSL_API_3_0) int i, num_locks; size_t size; #endif if (ebuf_len > 0) { ebuf[0] = 0; } #if !defined(NO_SSL_DL) if (!cryptolib_dll_handle) { memset(tls_feature_missing, 0, sizeof(tls_feature_missing)); cryptolib_dll_handle = load_tls_dll( ebuf, ebuf_len, CRYPTO_LIB, crypto_sw, tls_feature_missing); if (!cryptolib_dll_handle) { mg_snprintf(NULL, NULL, /* No truncation check for ebuf */ ebuf, ebuf_len, "%s: error loading library %s", __func__, CRYPTO_LIB); DEBUG_TRACE("%s", ebuf); return 0; } } #endif /* NO_SSL_DL */ if (mg_atomic_inc(&cryptolib_users) > 1) { return 1; } #if !defined(OPENSSL_API_1_1) && !defined(OPENSSL_API_3_0) /* Initialize locking callbacks, needed for thread safety. * http://www.openssl.org/support/faq.html#PROG1 */ num_locks = CRYPTO_num_locks(); if (num_locks < 0) { num_locks = 0; } size = sizeof(pthread_mutex_t) * ((size_t)(num_locks)); /* allocate mutex array, if required */ if (num_locks == 0) { /* No mutex array required */ ssl_mutexes = NULL; } else { /* Mutex array required - allocate it */ ssl_mutexes = (pthread_mutex_t *)mg_malloc(size); /* Check OOM */ if (ssl_mutexes == NULL) { mg_snprintf(NULL, NULL, /* No truncation check for ebuf */ ebuf, ebuf_len, "%s: cannot allocate mutexes: %s", __func__, ssl_error()); DEBUG_TRACE("%s", ebuf); return 0; } /* initialize mutex array */ for (i = 0; i < num_locks; i++) { if (0 != pthread_mutex_init(&ssl_mutexes[i], &pthread_mutex_attr)) { mg_snprintf(NULL, NULL, /* No truncation check for ebuf */ ebuf, ebuf_len, "%s: error initializing mutex %i of %i", __func__, i, num_locks); DEBUG_TRACE("%s", ebuf); mg_free(ssl_mutexes); return 0; } } } CRYPTO_set_locking_callback(&ssl_locking_callback); CRYPTO_set_id_callback(&mg_current_thread_id); #endif /* OPENSSL_API_1_1 || OPENSSL_API_3_0 */ #if !defined(NO_SSL_DL) if (!ssllib_dll_handle) { ssllib_dll_handle = load_tls_dll(ebuf, ebuf_len, SSL_LIB, ssl_sw, tls_feature_missing); if (!ssllib_dll_handle) { #if !defined(OPENSSL_API_1_1) mg_free(ssl_mutexes); #endif DEBUG_TRACE("%s", ebuf); return 0; } } #endif /* NO_SSL_DL */ #if (defined(OPENSSL_API_1_1) || defined(OPENSSL_API_3_0)) \ && !defined(NO_SSL_DL) /* Initialize SSL library */ OPENSSL_init_ssl(0, NULL); OPENSSL_init_ssl(OPENSSL_INIT_LOAD_SSL_STRINGS | OPENSSL_INIT_LOAD_CRYPTO_STRINGS, NULL); #else /* Initialize SSL library */ SSL_library_init(); SSL_load_error_strings(); #endif return 1; } static int ssl_use_pem_file(struct mg_context *phys_ctx, struct mg_domain_context *dom_ctx, const char *pem, const char *chain) { if (SSL_CTX_use_certificate_file(dom_ctx->ssl_ctx, pem, 1) == 0) { mg_cry_ctx_internal(phys_ctx, "%s: cannot open certificate file %s: %s", __func__, pem, ssl_error()); return 0; } /* could use SSL_CTX_set_default_passwd_cb_userdata */ if (SSL_CTX_use_PrivateKey_file(dom_ctx->ssl_ctx, pem, 1) == 0) { mg_cry_ctx_internal(phys_ctx, "%s: cannot open private key file %s: %s", __func__, pem, ssl_error()); return 0; } if (SSL_CTX_check_private_key(dom_ctx->ssl_ctx) == 0) { mg_cry_ctx_internal(phys_ctx, "%s: certificate and private key do not match: %s", __func__, pem); return 0; } /* In contrast to OpenSSL, wolfSSL does not support certificate * chain files that contain private keys and certificates in * SSL_CTX_use_certificate_chain_file. * The CivetWeb-Server used pem-Files that contained both information. * In order to make wolfSSL work, it is split in two files. * One file that contains key and certificate used by the server and * an optional chain file for the ssl stack. */ if (chain) { if (SSL_CTX_use_certificate_chain_file(dom_ctx->ssl_ctx, chain) == 0) { mg_cry_ctx_internal(phys_ctx, "%s: cannot use certificate chain file %s: %s", __func__, chain, ssl_error()); return 0; } } return 1; } #if defined(OPENSSL_API_1_1) static unsigned long ssl_get_protocol(int version_id) { long unsigned ret = (long unsigned)SSL_OP_ALL; if (version_id > 0) ret |= SSL_OP_NO_SSLv2; if (version_id > 1) ret |= SSL_OP_NO_SSLv3; if (version_id > 2) ret |= SSL_OP_NO_TLSv1; if (version_id > 3) ret |= SSL_OP_NO_TLSv1_1; if (version_id > 4) ret |= SSL_OP_NO_TLSv1_2; #if defined(SSL_OP_NO_TLSv1_3) if (version_id > 5) ret |= SSL_OP_NO_TLSv1_3; #endif return ret; } #else static long ssl_get_protocol(int version_id) { unsigned long ret = (unsigned long)SSL_OP_ALL; if (version_id > 0) ret |= SSL_OP_NO_SSLv2; if (version_id > 1) ret |= SSL_OP_NO_SSLv3; if (version_id > 2) ret |= SSL_OP_NO_TLSv1; if (version_id > 3) ret |= SSL_OP_NO_TLSv1_1; if (version_id > 4) ret |= SSL_OP_NO_TLSv1_2; #if defined(SSL_OP_NO_TLSv1_3) if (version_id > 5) ret |= SSL_OP_NO_TLSv1_3; #endif return (long)ret; } #endif /* OPENSSL_API_1_1 */ /* SSL callback documentation: * https://www.openssl.org/docs/man1.1.0/ssl/SSL_set_info_callback.html * https://wiki.openssl.org/index.php/Manual:SSL_CTX_set_info_callback(3) * https://linux.die.net/man/3/ssl_set_info_callback */ /* Note: There is no "const" for the first argument in the documentation * examples, however some (maybe most, but not all) headers of OpenSSL * versions / OpenSSL compatibility layers have it. Having a different * definition will cause a warning in C and an error in C++. Use "const SSL * *", while automatic conversion from "SSL *" works for all compilers, * but not other way around */ static void ssl_info_callback(const SSL *ssl, int what, int ret) { (void)ret; if (what & SSL_CB_HANDSHAKE_START) { SSL_get_app_data(ssl); } if (what & SSL_CB_HANDSHAKE_DONE) { /* TODO: check for openSSL 1.1 */ //#define SSL3_FLAGS_NO_RENEGOTIATE_CIPHERS 0x0001 // ssl->s3->flags |= SSL3_FLAGS_NO_RENEGOTIATE_CIPHERS; } } static int ssl_servername_callback(SSL *ssl, int *ad, void *arg) { #if defined(GCC_DIAGNOSTIC) # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wcast-align" #endif /* defined(GCC_DIAGNOSTIC) */ /* We used an aligned pointer in SSL_set_app_data */ struct mg_connection *conn = (struct mg_connection *)SSL_get_app_data(ssl); #if defined(GCC_DIAGNOSTIC) # pragma GCC diagnostic pop #endif /* defined(GCC_DIAGNOSTIC) */ const char *servername = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name); (void)ad; (void)arg; if ((conn == NULL) || (conn->phys_ctx == NULL)) { DEBUG_ASSERT(0); return SSL_TLSEXT_ERR_NOACK; } conn->dom_ctx = &(conn->phys_ctx->dd); /* Old clients (Win XP) will not support SNI. Then, there * is no server name available in the request - we can * only work with the default certificate. * Multiple HTTPS hosts on one IP+port are only possible * with a certificate containing all alternative names. */ if ((servername == NULL) || (*servername == 0)) { DEBUG_TRACE("%s", "SSL connection not supporting SNI"); mg_lock_context(conn->phys_ctx); SSL_set_SSL_CTX(ssl, conn->dom_ctx->ssl_ctx); mg_unlock_context(conn->phys_ctx); return SSL_TLSEXT_ERR_NOACK; } DEBUG_TRACE("TLS connection to host %s", servername); while (conn->dom_ctx) { if (!mg_strcasecmp(servername, conn->dom_ctx->config[AUTHENTICATION_DOMAIN])) { /* Found matching domain */ DEBUG_TRACE("TLS domain %s found", conn->dom_ctx->config[AUTHENTICATION_DOMAIN]); break; } mg_lock_context(conn->phys_ctx); conn->dom_ctx = conn->dom_ctx->next; mg_unlock_context(conn->phys_ctx); } if (conn->dom_ctx == NULL) { /* Default domain */ DEBUG_TRACE("TLS default domain %s used", conn->phys_ctx->dd.config[AUTHENTICATION_DOMAIN]); conn->dom_ctx = &(conn->phys_ctx->dd); } mg_lock_context(conn->phys_ctx); SSL_set_SSL_CTX(ssl, conn->dom_ctx->ssl_ctx); mg_unlock_context(conn->phys_ctx); return SSL_TLSEXT_ERR_OK; } #if defined(USE_ALPN) static const char alpn_proto_list[] = "\x02h2\x08http/1.1\x08http/1.0"; static const char *alpn_proto_order_http1[] = {alpn_proto_list + 3, alpn_proto_list + 3 + 8, NULL}; #if defined(USE_HTTP2) static const char *alpn_proto_order_http2[] = {alpn_proto_list, alpn_proto_list + 3, alpn_proto_list + 3 + 8, NULL}; #endif static int alpn_select_cb(SSL *ssl, const unsigned char **out, unsigned char *outlen, const unsigned char *in, unsigned int inlen, void *arg) { struct mg_domain_context *dom_ctx = (struct mg_domain_context *)arg; unsigned int i, j, enable_http2 = 0; const char **alpn_proto_order = alpn_proto_order_http1; struct mg_workerTLS *tls = (struct mg_workerTLS *)pthread_getspecific(sTlsKey); (void)ssl; if (tls == NULL) { /* Need to store protocol in Thread Local Storage */ /* If there is no Thread Local Storage, don't use ALPN */ return SSL_TLSEXT_ERR_NOACK; } #if defined(USE_HTTP2) enable_http2 = (0 == strcmp(dom_ctx->config[ENABLE_HTTP2], "yes")); if (enable_http2) { alpn_proto_order = alpn_proto_order_http2; } #endif for (j = 0; alpn_proto_order[j] != NULL; j++) { /* check all accepted protocols in this order */ const char *alpn_proto = alpn_proto_order[j]; /* search input for matching protocol */ for (i = 0; i < inlen; i++) { if (!memcmp(in + i, alpn_proto, (unsigned char)alpn_proto[0])) { *out = in + i + 1; *outlen = in[i]; tls->alpn_proto = alpn_proto; return SSL_TLSEXT_ERR_OK; } } } /* Nothing found */ return SSL_TLSEXT_ERR_NOACK; } static int next_protos_advertised_cb(SSL *ssl, const unsigned char **data, unsigned int *len, void *arg) { struct mg_domain_context *dom_ctx = (struct mg_domain_context *)arg; *data = (const unsigned char *)alpn_proto_list; *len = (unsigned int)strlen((const char *)data); (void)ssl; (void)dom_ctx; return SSL_TLSEXT_ERR_OK; } static int init_alpn(struct mg_context *phys_ctx, struct mg_domain_context *dom_ctx) { unsigned int alpn_len = (unsigned int)strlen((char *)alpn_proto_list); int ret = SSL_CTX_set_alpn_protos(dom_ctx->ssl_ctx, (const unsigned char *)alpn_proto_list, alpn_len); if (ret != 0) { mg_cry_ctx_internal(phys_ctx, "SSL_CTX_set_alpn_protos error: %s", ssl_error()); } SSL_CTX_set_alpn_select_cb(dom_ctx->ssl_ctx, alpn_select_cb, (void *)dom_ctx); SSL_CTX_set_next_protos_advertised_cb(dom_ctx->ssl_ctx, next_protos_advertised_cb, (void *)dom_ctx); return ret; } #endif /* Setup SSL CTX as required by CivetWeb */ static int init_ssl_ctx_impl(struct mg_context *phys_ctx, struct mg_domain_context *dom_ctx, const char *pem, const char *chain) { int callback_ret; int should_verify_peer; int peer_certificate_optional; const char *ca_path; const char *ca_file; int use_default_verify_paths; int verify_depth; struct timespec now_mt; md5_byte_t ssl_context_id[16]; md5_state_t md5state; int protocol_ver; int ssl_cache_timeout; #if (defined(OPENSSL_API_1_1) || defined(OPENSSL_API_3_0)) \ && !defined(NO_SSL_DL) if ((dom_ctx->ssl_ctx = SSL_CTX_new(TLS_server_method())) == NULL) { mg_cry_ctx_internal(phys_ctx, "SSL_CTX_new (server) error: %s", ssl_error()); return 0; } #else if ((dom_ctx->ssl_ctx = SSL_CTX_new(SSLv23_server_method())) == NULL) { mg_cry_ctx_internal(phys_ctx, "SSL_CTX_new (server) error: %s", ssl_error()); return 0; } #endif /* OPENSSL_API_1_1 || OPENSSL_API_3_0 */ #if defined(SSL_OP_NO_TLSv1_3) SSL_CTX_clear_options(dom_ctx->ssl_ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_TLSv1 | SSL_OP_NO_TLSv1_1 | SSL_OP_NO_TLSv1_2 | SSL_OP_NO_TLSv1_3); #else SSL_CTX_clear_options(dom_ctx->ssl_ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_TLSv1 | SSL_OP_NO_TLSv1_1 | SSL_OP_NO_TLSv1_2); #endif protocol_ver = atoi(dom_ctx->config[SSL_PROTOCOL_VERSION]); SSL_CTX_set_options(dom_ctx->ssl_ctx, ssl_get_protocol(protocol_ver)); SSL_CTX_set_options(dom_ctx->ssl_ctx, SSL_OP_SINGLE_DH_USE); SSL_CTX_set_options(dom_ctx->ssl_ctx, SSL_OP_CIPHER_SERVER_PREFERENCE); SSL_CTX_set_options(dom_ctx->ssl_ctx, SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION); SSL_CTX_set_options(dom_ctx->ssl_ctx, SSL_OP_NO_COMPRESSION); #if defined(SSL_OP_NO_RENEGOTIATION) SSL_CTX_set_options(dom_ctx->ssl_ctx, SSL_OP_NO_RENEGOTIATION); #endif #if !defined(NO_SSL_DL) SSL_CTX_set_ecdh_auto(dom_ctx->ssl_ctx, 1); #endif /* NO_SSL_DL */ /* In SSL documentation examples callback defined without const * specifier 'void (*)(SSL *, int, int)' See: * https://www.openssl.org/docs/man1.0.2/ssl/ssl.html * https://www.openssl.org/docs/man1.1.0/ssl/ssl.html * But in the source code const SSL is used: * 'void (*)(const SSL *, int, int)' See: * https://github.com/openssl/openssl/blob/1d97c8435171a7af575f73c526d79e1ef0ee5960/ssl/ssl.h#L1173 * Problem about wrong documentation described, but not resolved: * https://bugs.launchpad.net/ubuntu/+source/openssl/+bug/1147526 * Wrong const cast ignored on C or can be suppressed by compiler flags. * But when compiled with modern C++ compiler, correct const should be * provided */ SSL_CTX_set_info_callback(dom_ctx->ssl_ctx, ssl_info_callback); SSL_CTX_set_tlsext_servername_callback(dom_ctx->ssl_ctx, ssl_servername_callback); /* If a callback has been specified, call it. */ callback_ret = (phys_ctx->callbacks.init_ssl == NULL) ? 0 : (phys_ctx->callbacks.init_ssl(dom_ctx->ssl_ctx, phys_ctx->user_data)); /* If callback returns 0, civetweb sets up the SSL certificate. * If it returns 1, civetweb assumes the callback already did this. * If it returns -1, initializing ssl fails. */ if (callback_ret < 0) { mg_cry_ctx_internal(phys_ctx, "SSL callback returned error: %i", callback_ret); return 0; } if (callback_ret > 0) { /* Callback did everything. */ return 1; } /* If a domain callback has been specified, call it. */ callback_ret = (phys_ctx->callbacks.init_ssl_domain == NULL) ? 0 : (phys_ctx->callbacks.init_ssl_domain( dom_ctx->config[AUTHENTICATION_DOMAIN], dom_ctx->ssl_ctx, phys_ctx->user_data)); /* If domain callback returns 0, civetweb sets up the SSL certificate. * If it returns 1, civetweb assumes the callback already did this. * If it returns -1, initializing ssl fails. */ if (callback_ret < 0) { mg_cry_ctx_internal(phys_ctx, "Domain SSL callback returned error: %i", callback_ret); return 0; } if (callback_ret > 0) { /* Domain callback did everything. */ return 1; } /* Use some combination of start time, domain and port as a SSL * context ID. This should be unique on the current machine. */ md5_init(&md5state); clock_gettime(CLOCK_MONOTONIC, &now_mt); md5_append(&md5state, (const md5_byte_t *)&now_mt, sizeof(now_mt)); md5_append(&md5state, (const md5_byte_t *)phys_ctx->dd.config[LISTENING_PORTS], strlen(phys_ctx->dd.config[LISTENING_PORTS])); md5_append(&md5state, (const md5_byte_t *)dom_ctx->config[AUTHENTICATION_DOMAIN], strlen(dom_ctx->config[AUTHENTICATION_DOMAIN])); md5_append(&md5state, (const md5_byte_t *)phys_ctx, sizeof(*phys_ctx)); md5_append(&md5state, (const md5_byte_t *)dom_ctx, sizeof(*dom_ctx)); md5_finish(&md5state, ssl_context_id); SSL_CTX_set_session_id_context(dom_ctx->ssl_ctx, (unsigned char *)ssl_context_id, sizeof(ssl_context_id)); if (pem != NULL) { if (!ssl_use_pem_file(phys_ctx, dom_ctx, pem, chain)) { return 0; } } /* Should we support client certificates? */ /* Default is "no". */ should_verify_peer = 0; peer_certificate_optional = 0; if (dom_ctx->config[SSL_DO_VERIFY_PEER] != NULL) { if (mg_strcasecmp(dom_ctx->config[SSL_DO_VERIFY_PEER], "yes") == 0) { /* Yes, they are mandatory */ should_verify_peer = 1; } else if (mg_strcasecmp(dom_ctx->config[SSL_DO_VERIFY_PEER], "optional") == 0) { /* Yes, they are optional */ should_verify_peer = 1; peer_certificate_optional = 1; } } use_default_verify_paths = (dom_ctx->config[SSL_DEFAULT_VERIFY_PATHS] != NULL) && (mg_strcasecmp(dom_ctx->config[SSL_DEFAULT_VERIFY_PATHS], "yes") == 0); if (should_verify_peer) { ca_path = dom_ctx->config[SSL_CA_PATH]; ca_file = dom_ctx->config[SSL_CA_FILE]; if (SSL_CTX_load_verify_locations(dom_ctx->ssl_ctx, ca_file, ca_path) != 1) { mg_cry_ctx_internal(phys_ctx, "SSL_CTX_load_verify_locations error: %s " "ssl_verify_peer requires setting " "either ssl_ca_path or ssl_ca_file. " "Is any of them present in the " ".conf file?", ssl_error()); return 0; } if (peer_certificate_optional) { SSL_CTX_set_verify(dom_ctx->ssl_ctx, SSL_VERIFY_PEER, NULL); } else { SSL_CTX_set_verify(dom_ctx->ssl_ctx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL); } if (use_default_verify_paths && (SSL_CTX_set_default_verify_paths(dom_ctx->ssl_ctx) != 1)) { mg_cry_ctx_internal(phys_ctx, "SSL_CTX_set_default_verify_paths error: %s", ssl_error()); return 0; } if (dom_ctx->config[SSL_VERIFY_DEPTH]) { verify_depth = atoi(dom_ctx->config[SSL_VERIFY_DEPTH]); SSL_CTX_set_verify_depth(dom_ctx->ssl_ctx, verify_depth); } } if (dom_ctx->config[SSL_CIPHER_LIST] != NULL) { if (SSL_CTX_set_cipher_list(dom_ctx->ssl_ctx, dom_ctx->config[SSL_CIPHER_LIST]) != 1) { mg_cry_ctx_internal(phys_ctx, "SSL_CTX_set_cipher_list error: %s", ssl_error()); } } /* SSL session caching */ ssl_cache_timeout = ((dom_ctx->config[SSL_CACHE_TIMEOUT] != NULL) ? atoi(dom_ctx->config[SSL_CACHE_TIMEOUT]) : 0); if (ssl_cache_timeout > 0) { SSL_CTX_set_session_cache_mode(dom_ctx->ssl_ctx, SSL_SESS_CACHE_BOTH); /* SSL_CTX_sess_set_cache_size(dom_ctx->ssl_ctx, 10000); ... use * default */ SSL_CTX_set_timeout(dom_ctx->ssl_ctx, (long)ssl_cache_timeout); } #if defined(USE_ALPN) /* Initialize ALPN only of TLS library (OpenSSL version) supports ALPN */ #if !defined(NO_SSL_DL) if (!tls_feature_missing[TLS_ALPN]) #endif { init_alpn(phys_ctx, dom_ctx); } #endif return 1; } /* Check if SSL is required. * If so, dynamically load SSL library * and set up ctx->ssl_ctx pointer. */ static int init_ssl_ctx(struct mg_context *phys_ctx, struct mg_domain_context *dom_ctx) { void *ssl_ctx = 0; int callback_ret; const char *pem; const char *chain; char ebuf[128]; if (!phys_ctx) { return 0; } if (!dom_ctx) { dom_ctx = &(phys_ctx->dd); } if (!is_ssl_port_used(dom_ctx->config[LISTENING_PORTS])) { /* No SSL port is set. No need to setup SSL. */ return 1; } /* Check for external SSL_CTX */ callback_ret = (phys_ctx->callbacks.external_ssl_ctx == NULL) ? 0 : (phys_ctx->callbacks.external_ssl_ctx(&ssl_ctx, phys_ctx->user_data)); if (callback_ret < 0) { /* Callback exists and returns <0: Initializing failed. */ mg_cry_ctx_internal(phys_ctx, "external_ssl_ctx callback returned error: %i", callback_ret); return 0; } else if (callback_ret > 0) { /* Callback exists and returns >0: Initializing complete, * civetweb should not modify the SSL context. */ dom_ctx->ssl_ctx = (SSL_CTX *)ssl_ctx; if (!initialize_openssl(ebuf, sizeof(ebuf))) { mg_cry_ctx_internal(phys_ctx, "%s", ebuf); return 0; } return 1; } /* If the callback does not exist or return 0, civetweb must initialize * the SSL context. Handle "domain" callback next. */ /* Check for external domain SSL_CTX callback. */ callback_ret = (phys_ctx->callbacks.external_ssl_ctx_domain == NULL) ? 0 : (phys_ctx->callbacks.external_ssl_ctx_domain( dom_ctx->config[AUTHENTICATION_DOMAIN], &ssl_ctx, phys_ctx->user_data)); if (callback_ret < 0) { /* Callback < 0: Error. Abort init. */ mg_cry_ctx_internal( phys_ctx, "external_ssl_ctx_domain callback returned error: %i", callback_ret); return 0; } else if (callback_ret > 0) { /* Callback > 0: Consider init done. */ dom_ctx->ssl_ctx = (SSL_CTX *)ssl_ctx; if (!initialize_openssl(ebuf, sizeof(ebuf))) { mg_cry_ctx_internal(phys_ctx, "%s", ebuf); return 0; } return 1; } /* else: external_ssl_ctx/external_ssl_ctx_domain do not exist or return * 0, CivetWeb should continue initializing SSL */ /* If PEM file is not specified and the init_ssl callbacks * are not specified, setup will fail. */ if (((pem = dom_ctx->config[SSL_CERTIFICATE]) == NULL) && (phys_ctx->callbacks.init_ssl == NULL) && (phys_ctx->callbacks.init_ssl_domain == NULL)) { /* No certificate and no init_ssl callbacks: * Essential data to set up TLS is missing. */ mg_cry_ctx_internal(phys_ctx, "Initializing SSL failed: -%s is not set", config_options[SSL_CERTIFICATE].name); return 0; } /* If a certificate chain is configured, use it. */ chain = dom_ctx->config[SSL_CERTIFICATE_CHAIN]; if (chain == NULL) { /* Default: certificate chain in PEM file */ chain = pem; } if ((chain != NULL) && (*chain == 0)) { /* If the chain is an empty string, don't use it. */ chain = NULL; } if (!initialize_openssl(ebuf, sizeof(ebuf))) { mg_cry_ctx_internal(phys_ctx, "%s", ebuf); return 0; } return init_ssl_ctx_impl(phys_ctx, dom_ctx, pem, chain); } static void uninitialize_openssl(void) { #if defined(OPENSSL_API_1_1) || defined(OPENSSL_API_3_0) if (mg_atomic_dec(&cryptolib_users) == 0) { /* Shutdown according to * https://wiki.openssl.org/index.php/Library_Initialization#Cleanup * http://stackoverflow.com/questions/29845527/how-to-properly-uninitialize-openssl */ CONF_modules_unload(1); #else int i; if (mg_atomic_dec(&cryptolib_users) == 0) { /* Shutdown according to * https://wiki.openssl.org/index.php/Library_Initialization#Cleanup * http://stackoverflow.com/questions/29845527/how-to-properly-uninitialize-openssl */ CRYPTO_set_locking_callback(NULL); CRYPTO_set_id_callback(NULL); ENGINE_cleanup(); CONF_modules_unload(1); ERR_free_strings(); EVP_cleanup(); CRYPTO_cleanup_all_ex_data(); OPENSSL_REMOVE_THREAD_STATE(); for (i = 0; i < CRYPTO_num_locks(); i++) { pthread_mutex_destroy(&ssl_mutexes[i]); } mg_free(ssl_mutexes); ssl_mutexes = NULL; #endif /* OPENSSL_API_1_1 || OPENSSL_API_3_0 */ } } #endif /* !defined(NO_SSL) && !defined(USE_MBEDTLS) */ #if !defined(NO_FILESYSTEMS) static int set_gpass_option(struct mg_context *phys_ctx, struct mg_domain_context *dom_ctx) { if (phys_ctx) { struct mg_file file = STRUCT_FILE_INITIALIZER; const char *path; struct mg_connection fc; if (!dom_ctx) { dom_ctx = &(phys_ctx->dd); } path = dom_ctx->config[GLOBAL_PASSWORDS_FILE]; if ((path != NULL) && !mg_stat(fake_connection(&fc, phys_ctx), path, &file.stat)) { mg_cry_ctx_internal(phys_ctx, "Cannot open %s: %s", path, strerror(ERRNO)); return 0; } return 1; } return 0; } #endif /* NO_FILESYSTEMS */ static int set_acl_option(struct mg_context *phys_ctx) { union usa sa; memset(&sa, 0, sizeof(sa)); #if defined(USE_IPV6) sa.sin6.sin6_family = AF_INET6; #else sa.sin.sin_family = AF_INET; #endif return check_acl(phys_ctx, &sa) != -1; } static void reset_per_request_attributes(struct mg_connection *conn) { if (!conn) { return; } conn->num_bytes_sent = conn->consumed_content = 0; conn->path_info = NULL; conn->status_code = -1; conn->content_len = -1; conn->is_chunked = 0; conn->must_close = 0; conn->request_len = 0; conn->request_state = 0; conn->throttle = 0; conn->accept_gzip = 0; conn->response_info.content_length = conn->request_info.content_length = -1; conn->response_info.http_version = conn->request_info.http_version = NULL; conn->response_info.num_headers = conn->request_info.num_headers = 0; conn->response_info.status_text = NULL; conn->response_info.status_code = 0; conn->request_info.remote_user = NULL; conn->request_info.request_method = NULL; conn->request_info.request_uri = NULL; /* Free cleaned local URI (if any) */ if (conn->request_info.local_uri != conn->request_info.local_uri_raw) { mg_free((void *)conn->request_info.local_uri); conn->request_info.local_uri = NULL; } conn->request_info.local_uri = NULL; #if defined(USE_SERVER_STATS) conn->processing_time = 0; #endif } static int set_tcp_nodelay(const struct socket *so, int nodelay_on) { if ((so->lsa.sa.sa_family == AF_INET) || (so->lsa.sa.sa_family == AF_INET6)) { /* Only for TCP sockets */ if (setsockopt(so->sock, IPPROTO_TCP, TCP_NODELAY, (SOCK_OPT_TYPE)&nodelay_on, sizeof(nodelay_on)) != 0) { /* Error */ return 1; } } /* OK */ return 0; } #if !defined(__ZEPHYR__) static void close_socket_gracefully(struct mg_connection *conn) { #if defined(_WIN32) char buf[MG_BUF_LEN]; int n; #endif struct linger linger; int error_code = 0; int linger_timeout = -2; socklen_t opt_len = sizeof(error_code); if (!conn) { return; } /* http://msdn.microsoft.com/en-us/library/ms739165(v=vs.85).aspx: * "Note that enabling a nonzero timeout on a nonblocking socket * is not recommended.", so set it to blocking now */ set_blocking_mode(conn->client.sock); /* Send FIN to the client */ shutdown(conn->client.sock, SHUTDOWN_WR); #if defined(_WIN32) /* Read and discard pending incoming data. If we do not do that and * close * the socket, the data in the send buffer may be discarded. This * behaviour is seen on Windows, when client keeps sending data * when server decides to close the connection; then when client * does recv() it gets no data back. */ do { n = pull_inner(NULL, conn, buf, sizeof(buf), /* Timeout in s: */ 1.0); } while (n > 0); #endif if (conn->dom_ctx->config[LINGER_TIMEOUT]) { linger_timeout = atoi(conn->dom_ctx->config[LINGER_TIMEOUT]); } /* Set linger option according to configuration */ if (linger_timeout >= 0) { /* Set linger option to avoid socket hanging out after close. This * prevent ephemeral port exhaust problem under high QPS. */ linger.l_onoff = 1; #if defined(_MSC_VER) # pragma warning(push) # pragma warning(disable : 4244) #endif #if defined(GCC_DIAGNOSTIC) # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wconversion" #endif /* Data type of linger structure elements may differ, * so we don't know what cast we need here. * Disable type conversion warnings. */ linger.l_linger = (linger_timeout + 999) / 1000; #if defined(GCC_DIAGNOSTIC) # pragma GCC diagnostic pop #endif #if defined(_MSC_VER) # pragma warning(pop) #endif } else { linger.l_onoff = 0; linger.l_linger = 0; } if (linger_timeout < -1) { /* Default: don't configure any linger */ } else if (getsockopt(conn->client.sock, SOL_SOCKET, SO_ERROR, #if defined(_WIN32) /* WinSock uses different data type here */ (char *)&error_code, #else &error_code, #endif &opt_len) != 0) { /* Cannot determine if socket is already closed. This should * not occur and never did in a test. Log an error message * and continue. */ mg_cry_internal(conn, "%s: getsockopt(SOL_SOCKET SO_ERROR) failed: %s", __func__, strerror(ERRNO)); #if defined(_WIN32) } else if (error_code == WSAECONNRESET) { #else } else if (error_code == ECONNRESET) { #endif /* Socket already closed by client/peer, close socket without linger */ } else { /* Set linger timeout */ if (setsockopt(conn->client.sock, SOL_SOCKET, SO_LINGER, (char *)&linger, sizeof(linger)) != 0) { mg_cry_internal( conn, "%s: setsockopt(SOL_SOCKET SO_LINGER(%i,%i)) failed: %s", __func__, linger.l_onoff, linger.l_linger, strerror(ERRNO)); } } /* Now we know that our FIN is ACK-ed, safe to close */ closesocket(conn->client.sock); conn->client.sock = INVALID_SOCKET; } #endif static void close_connection(struct mg_connection *conn) { #if defined(USE_SERVER_STATS) conn->conn_state = 6; /* to close */ #endif #if defined(USE_LUA) && defined(USE_WEBSOCKET) if (conn->lua_websocket_state) { lua_websocket_close(conn, conn->lua_websocket_state); conn->lua_websocket_state = NULL; } #endif mg_lock_connection(conn); /* Set close flag, so keep-alive loops will stop */ conn->must_close = 1; /* call the connection_close callback if assigned */ if (conn->phys_ctx->callbacks.connection_close != NULL) { if (conn->phys_ctx->context_type == CONTEXT_SERVER) { conn->phys_ctx->callbacks.connection_close(conn); } } /* Reset user data, after close callback is called. * Do not reuse it. If the user needs a destructor, * it must be done in the connection_close callback. */ mg_set_user_connection_data(conn, NULL); #if defined(USE_SERVER_STATS) conn->conn_state = 7; /* closing */ #endif #if defined(USE_MBEDTLS) if (conn->ssl != NULL) { mbed_ssl_close(conn->ssl); conn->ssl = NULL; } #elif !defined(NO_SSL) if (conn->ssl != NULL) { /* Run SSL_shutdown twice to ensure completely close SSL connection */ SSL_shutdown(conn->ssl); SSL_free(conn->ssl); OPENSSL_REMOVE_THREAD_STATE(); conn->ssl = NULL; } #endif if (conn->client.sock != INVALID_SOCKET) { #if defined(__ZEPHYR__) closesocket(conn->client.sock); #else close_socket_gracefully(conn); #endif conn->client.sock = INVALID_SOCKET; } /* call the connection_closed callback if assigned */ if (conn->phys_ctx->callbacks.connection_closed != NULL) { if (conn->phys_ctx->context_type == CONTEXT_SERVER) { conn->phys_ctx->callbacks.connection_closed(conn); } } mg_unlock_connection(conn); #if defined(USE_SERVER_STATS) conn->conn_state = 8; /* closed */ #endif } CIVETWEB_API void mg_close_connection(struct mg_connection *conn) { if ((conn == NULL) || (conn->phys_ctx == NULL)) { return; } #if defined(USE_WEBSOCKET) if (conn->phys_ctx->context_type == CONTEXT_SERVER) { if (conn->in_websocket_handling) { /* Set close flag, so the server thread can exit. */ conn->must_close = 1; return; } } if (conn->phys_ctx->context_type == CONTEXT_WS_CLIENT) { unsigned int i; /* client context: loops must end */ STOP_FLAG_ASSIGN(&conn->phys_ctx->stop_flag, 1); conn->must_close = 1; /* We need to get the client thread out of the select/recv call * here. */ /* Since we use a sleep quantum of some seconds to check for recv * timeouts, we will just wait a few seconds in mg_join_thread. */ /* join worker thread */ for (i = 0; i < conn->phys_ctx->cfg_worker_threads; i++) { mg_join_thread(conn->phys_ctx->worker_threadids[i]); } } #endif /* defined(USE_WEBSOCKET) */ close_connection(conn); #if !defined(NO_SSL) && !defined(USE_MBEDTLS) // TODO: mbedTLS client if (((conn->phys_ctx->context_type == CONTEXT_HTTP_CLIENT) || (conn->phys_ctx->context_type == CONTEXT_WS_CLIENT)) && (conn->phys_ctx->dd.ssl_ctx != NULL)) { SSL_CTX_free(conn->phys_ctx->dd.ssl_ctx); } #endif #if defined(USE_WEBSOCKET) if (conn->phys_ctx->context_type == CONTEXT_WS_CLIENT) { mg_free(conn->phys_ctx->worker_threadids); (void)pthread_mutex_destroy(&conn->mutex); mg_free(conn); } else if (conn->phys_ctx->context_type == CONTEXT_HTTP_CLIENT) { (void)pthread_mutex_destroy(&conn->mutex); mg_free(conn); } #else if (conn->phys_ctx->context_type == CONTEXT_HTTP_CLIENT) { /* Client */ (void)pthread_mutex_destroy(&conn->mutex); mg_free(conn); } #endif /* defined(USE_WEBSOCKET) */ } static struct mg_connection * mg_connect_client_impl(const struct mg_client_options *client_options, int use_ssl, struct mg_init_data *init, struct mg_error_data *error) { struct mg_connection *conn = NULL; SOCKET sock; union usa sa; struct sockaddr *psa; socklen_t len; unsigned max_req_size = (unsigned)atoi(config_options[MAX_REQUEST_SIZE].default_value); /* Size of structures, aligned to 8 bytes */ size_t conn_size = ((sizeof(struct mg_connection) + 7) >> 3) << 3; size_t ctx_size = ((sizeof(struct mg_context) + 7) >> 3) << 3; size_t alloc_size = conn_size + ctx_size + max_req_size; (void)init; /* TODO: Implement required options */ conn = (struct mg_connection *)mg_calloc(1, alloc_size); if (error != NULL) { error->code = MG_ERROR_DATA_CODE_OK; error->code_sub = 0; if (error->text_buffer_size > 0) { error->text[0] = 0; } } if (conn == NULL) { if (error != NULL) { error->code = MG_ERROR_DATA_CODE_OUT_OF_MEMORY; error->code_sub = (unsigned)alloc_size; mg_snprintf(NULL, NULL, /* No truncation check for ebuf */ error->text, error->text_buffer_size, "calloc(): %s", strerror(ERRNO)); } return NULL; } #if defined(GCC_DIAGNOSTIC) # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wcast-align" #endif /* defined(GCC_DIAGNOSTIC) */ /* conn_size is aligned to 8 bytes */ conn->phys_ctx = (struct mg_context *)(((char *)conn) + conn_size); #if defined(GCC_DIAGNOSTIC) # pragma GCC diagnostic pop #endif /* defined(GCC_DIAGNOSTIC) */ conn->buf = (((char *)conn) + conn_size + ctx_size); conn->buf_size = (int)max_req_size; conn->phys_ctx->context_type = CONTEXT_HTTP_CLIENT; conn->dom_ctx = &(conn->phys_ctx->dd); if (!connect_socket(conn->phys_ctx, client_options->host, client_options->port, use_ssl, error, &sock, &sa)) { /* "error" will be set by connect_socket. */ /* free all memory and return NULL; */ mg_free(conn); return NULL; } #if !defined(NO_SSL) && !defined(USE_MBEDTLS) // TODO: mbedTLS client #if (defined(OPENSSL_API_1_1) || defined(OPENSSL_API_3_0)) \ && !defined(NO_SSL_DL) if (use_ssl && (conn->dom_ctx->ssl_ctx = SSL_CTX_new(TLS_client_method())) == NULL) { if (error != NULL) { error->code = MG_ERROR_DATA_CODE_INIT_TLS_FAILED; mg_snprintf(NULL, NULL, /* No truncation check for ebuf */ error->text, error->text_buffer_size, "SSL_CTX_new error: %s", ssl_error()); } closesocket(sock); mg_free(conn); return NULL; } #else if (use_ssl && (conn->dom_ctx->ssl_ctx = SSL_CTX_new(SSLv23_client_method())) == NULL) { if (error != NULL) { error->code = MG_ERROR_DATA_CODE_INIT_TLS_FAILED; mg_snprintf(NULL, NULL, /* No truncation check for ebuf */ error->text, error->text_buffer_size, "SSL_CTX_new error: %s", ssl_error()); } closesocket(sock); mg_free(conn); return NULL; } #endif /* OPENSSL_API_1_1 || OPENSSL_API_3_0 */ #endif /* NO_SSL */ #if defined(USE_IPV6) len = (sa.sa.sa_family == AF_INET) ? sizeof(conn->client.rsa.sin) : sizeof(conn->client.rsa.sin6); psa = (sa.sa.sa_family == AF_INET) ? (struct sockaddr *)&(conn->client.rsa.sin) : (struct sockaddr *)&(conn->client.rsa.sin6); #else len = sizeof(conn->client.rsa.sin); psa = (struct sockaddr *)&(conn->client.rsa.sin); #endif conn->client.sock = sock; conn->client.lsa = sa; if (getsockname(sock, psa, &len) != 0) { mg_cry_internal(conn, "%s: getsockname() failed: %s", __func__, strerror(ERRNO)); } conn->client.is_ssl = use_ssl ? 1 : 0; if (0 != pthread_mutex_init(&conn->mutex, &pthread_mutex_attr)) { if (error != NULL) { error->code = MG_ERROR_DATA_CODE_OS_ERROR; error->code_sub = (unsigned)ERRNO; mg_snprintf(NULL, NULL, /* No truncation check for ebuf */ error->text, error->text_buffer_size, "Can not create mutex"); } #if !defined(NO_SSL) && !defined(USE_MBEDTLS) // TODO: mbedTLS client SSL_CTX_free(conn->dom_ctx->ssl_ctx); #endif closesocket(sock); mg_free(conn); return NULL; } #if !defined(NO_SSL) && !defined(USE_MBEDTLS) // TODO: mbedTLS client if (use_ssl) { /* TODO: Check ssl_verify_peer and ssl_ca_path here. * SSL_CTX_set_verify call is needed to switch off server * certificate checking, which is off by default in OpenSSL and * on in yaSSL. */ /* TODO: SSL_CTX_set_verify(conn->dom_ctx, * SSL_VERIFY_PEER, verify_ssl_server); */ if (client_options->client_cert) { if (!ssl_use_pem_file(conn->phys_ctx, conn->dom_ctx, client_options->client_cert, NULL)) { if (error != NULL) { error->code = MG_ERROR_DATA_CODE_TLS_CLIENT_CERT_ERROR; mg_snprintf(NULL, NULL, /* No truncation check for ebuf */ error->text, error->text_buffer_size, "Can not use SSL client certificate"); } SSL_CTX_free(conn->dom_ctx->ssl_ctx); closesocket(sock); mg_free(conn); return NULL; } } if (client_options->server_cert) { if (SSL_CTX_load_verify_locations(conn->dom_ctx->ssl_ctx, client_options->server_cert, NULL) != 1) { if (error != NULL) { error->code = MG_ERROR_DATA_CODE_TLS_SERVER_CERT_ERROR; mg_snprintf(NULL, NULL, /* No truncation check for ebuf */ error->text, error->text_buffer_size, "SSL_CTX_load_verify_locations error: %s", ssl_error()); } SSL_CTX_free(conn->dom_ctx->ssl_ctx); closesocket(sock); mg_free(conn); return NULL; } SSL_CTX_set_verify(conn->dom_ctx->ssl_ctx, SSL_VERIFY_PEER, NULL); } else { SSL_CTX_set_verify(conn->dom_ctx->ssl_ctx, SSL_VERIFY_NONE, NULL); } if (!sslize(conn, SSL_connect, client_options)) { if (error != NULL) { error->code = MG_ERROR_DATA_CODE_TLS_CONNECT_ERROR; mg_snprintf(NULL, NULL, /* No truncation check for ebuf */ error->text, error->text_buffer_size, "SSL connection error"); } SSL_CTX_free(conn->dom_ctx->ssl_ctx); closesocket(sock); mg_free(conn); return NULL; } } #endif return conn; } CIVETWEB_API struct mg_connection * mg_connect_client_secure(const struct mg_client_options *client_options, char *error_buffer, size_t error_buffer_size) { struct mg_init_data init; struct mg_error_data error; memset(&init, 0, sizeof(init)); memset(&error, 0, sizeof(error)); error.text_buffer_size = error_buffer_size; error.text = error_buffer; return mg_connect_client_impl(client_options, 1, &init, &error); } CIVETWEB_API struct mg_connection * mg_connect_client(const char *host, int port, int use_ssl, char *error_buffer, size_t error_buffer_size) { struct mg_client_options opts; struct mg_init_data init; struct mg_error_data error; memset(&init, 0, sizeof(init)); memset(&error, 0, sizeof(error)); error.text_buffer_size = error_buffer_size; error.text = error_buffer; memset(&opts, 0, sizeof(opts)); opts.host = host; opts.port = port; if (use_ssl) { opts.host_name = host; } return mg_connect_client_impl(&opts, use_ssl, &init, &error); } #if defined(MG_EXPERIMENTAL_INTERFACES) CIVETWEB_API struct mg_connection * mg_connect_client2(const char *host, const char *protocol, int port, const char *path, struct mg_init_data *init, struct mg_error_data *error) { (void)path; int is_ssl, is_ws; /* void *user_data = (init != NULL) ? init->user_data : NULL; -- TODO */ if (error != NULL) { error->code = MG_ERROR_DATA_CODE_OK; error->code_sub = 0; if (error->text_buffer_size > 0) { *error->text = 0; } } if ((host == NULL) || (protocol == NULL)) { if (error != NULL) { error->code = MG_ERROR_DATA_CODE_INVALID_PARAM; mg_snprintf(NULL, NULL, /* No truncation check for error buffers */ error->text, error->text_buffer_size, "%s", "Invalid parameters"); } return NULL; } /* check all known protocols */ if (!mg_strcasecmp(protocol, "http")) { is_ssl = 0; is_ws = 0; } else if (!mg_strcasecmp(protocol, "https")) { is_ssl = 1; is_ws = 0; #if defined(USE_WEBSOCKET) } else if (!mg_strcasecmp(protocol, "ws")) { is_ssl = 0; is_ws = 1; } else if (!mg_strcasecmp(protocol, "wss")) { is_ssl = 1; is_ws = 1; #endif } else { if (error != NULL) { error->code = MG_ERROR_DATA_CODE_INVALID_PARAM; mg_snprintf(NULL, NULL, /* No truncation check for error buffers */ error->text, error->text_buffer_size, "Protocol %s not supported", protocol); } return NULL; } /* TODO: The current implementation here just calls the old * implementations, without using any new options. This is just a first * step to test the new interfaces. */ #if defined(USE_WEBSOCKET) if (is_ws) { /* TODO: implement all options */ return mg_connect_websocket_client( host, port, is_ssl, ((error != NULL) ? error->text : NULL), ((error != NULL) ? error->text_buffer_size : 0), (path ? path : ""), NULL /* TODO: origin */, experimental_websocket_client_data_wrapper, experimental_websocket_client_close_wrapper, (void *)init->callbacks); } #else (void)is_ws; #endif /* TODO: all additional options */ struct mg_client_options opts; memset(&opts, 0, sizeof(opts)); opts.host = host; opts.port = port; return mg_connect_client_impl(&opts, is_ssl, init, error); } #endif static const struct { const char *proto; size_t proto_len; unsigned default_port; } abs_uri_protocols[] = {{"http://", 7, 80}, {"https://", 8, 443}, {"ws://", 5, 80}, {"wss://", 6, 443}, {NULL, 0, 0}}; /* Check if the uri is valid. * return 0 for invalid uri, * return 1 for *, * return 2 for relative uri, * return 3 for absolute uri without port, * return 4 for absolute uri with port */ static int get_uri_type(const char *uri) { int i; const char *hostend, *portbegin; char *portend; unsigned long port; /* According to the HTTP standard * http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2 * URI can be an asterisk (*) or should start with slash (relative uri), * or it should start with the protocol (absolute uri). */ if ((uri[0] == '*') && (uri[1] == '\0')) { /* asterisk */ return 1; } /* Valid URIs according to RFC 3986 * (https://www.ietf.org/rfc/rfc3986.txt) * must only contain reserved characters :/?#[]@!$&'()*+,;= * and unreserved characters A-Z a-z 0-9 and -._~ * and % encoded symbols. */ for (i = 0; uri[i] != 0; i++) { if (uri[i] < 33) { /* control characters and spaces are invalid */ return 0; } /* Allow everything else here (See #894) */ } /* A relative uri starts with a / character */ if (uri[0] == '/') { /* relative uri */ return 2; } /* It could be an absolute uri: */ /* This function only checks if the uri is valid, not if it is * addressing the current server. So civetweb can also be used * as a proxy server. */ for (i = 0; abs_uri_protocols[i].proto != NULL; i++) { if (mg_strncasecmp(uri, abs_uri_protocols[i].proto, abs_uri_protocols[i].proto_len) == 0) { hostend = strchr(uri + abs_uri_protocols[i].proto_len, '/'); if (!hostend) { return 0; } portbegin = strchr(uri + abs_uri_protocols[i].proto_len, ':'); if (!portbegin) { return 3; } port = strtoul(portbegin + 1, &portend, 10); if ((portend != hostend) || (port <= 0) || !is_valid_port(port)) { return 0; } return 4; } } return 0; } /* Return NULL or the relative uri at the current server */ static const char * get_rel_url_at_current_server(const char *uri, const struct mg_connection *conn) { const char *server_domain; size_t server_domain_len; size_t request_domain_len = 0; unsigned long port = 0; int i, auth_domain_check_enabled; const char *hostbegin = NULL; const char *hostend = NULL; const char *portbegin; char *portend; auth_domain_check_enabled = !mg_strcasecmp(conn->dom_ctx->config[ENABLE_AUTH_DOMAIN_CHECK], "yes"); /* DNS is case insensitive, so use case insensitive string compare here */ for (i = 0; abs_uri_protocols[i].proto != NULL; i++) { if (mg_strncasecmp(uri, abs_uri_protocols[i].proto, abs_uri_protocols[i].proto_len) == 0) { hostbegin = uri + abs_uri_protocols[i].proto_len; hostend = strchr(hostbegin, '/'); if (!hostend) { return 0; } portbegin = strchr(hostbegin, ':'); if ((!portbegin) || (portbegin > hostend)) { port = abs_uri_protocols[i].default_port; request_domain_len = (size_t)(hostend - hostbegin); } else { port = strtoul(portbegin + 1, &portend, 10); if ((portend != hostend) || (port <= 0) || !is_valid_port(port)) { return 0; } request_domain_len = (size_t)(portbegin - hostbegin); } /* protocol found, port set */ break; } } if (!port) { /* port remains 0 if the protocol is not found */ return 0; } /* Check if the request is directed to a different server. */ /* First check if the port is the same. */ if (ntohs(USA_IN_PORT_UNSAFE(&conn->client.lsa)) != port) { /* Request is directed to a different port */ return 0; } /* Finally check if the server corresponds to the authentication * domain of the server (the server domain). * Allow full matches (like http://mydomain.com/path/file.ext), and * allow subdomain matches (like http://www.mydomain.com/path/file.ext), * but do not allow substrings (like * http://notmydomain.com/path/file.ext * or http://mydomain.com.fake/path/file.ext). */ if (auth_domain_check_enabled) { server_domain = conn->dom_ctx->config[AUTHENTICATION_DOMAIN]; server_domain_len = strlen(server_domain); if ((server_domain_len == 0) || (hostbegin == NULL)) { return 0; } if ((request_domain_len == server_domain_len) && (!memcmp(server_domain, hostbegin, server_domain_len))) { /* Request is directed to this server - full name match. */ } else { if (request_domain_len < (server_domain_len + 2)) { /* Request is directed to another server: The server name * is longer than the request name. * Drop this case here to avoid overflows in the * following checks. */ return 0; } if (hostbegin[request_domain_len - server_domain_len - 1] != '.') { /* Request is directed to another server: It could be a * substring * like notmyserver.com */ return 0; } if (0 != memcmp(server_domain, hostbegin + request_domain_len - server_domain_len, server_domain_len)) { /* Request is directed to another server: * The server name is different. */ return 0; } } } return hostend; } static int get_message(struct mg_connection *conn, char *ebuf, size_t ebuf_len, int *err) { if (ebuf_len > 0) { ebuf[0] = '\0'; } *err = 0; reset_per_request_attributes(conn); if (!conn) { mg_snprintf(conn, NULL, /* No truncation check for ebuf */ ebuf, ebuf_len, "%s", "Internal error"); *err = 500; return 0; } /* Set the time the request was received. This value should be used for * timeouts. */ clock_gettime(CLOCK_MONOTONIC, &(conn->req_time)); conn->request_len = read_message(NULL, conn, conn->buf, conn->buf_size, &conn->data_len); DEBUG_ASSERT(conn->request_len < 0 || conn->data_len >= conn->request_len); if ((conn->request_len >= 0) && (conn->data_len < conn->request_len)) { mg_snprintf(conn, NULL, /* No truncation check for ebuf */ ebuf, ebuf_len, "%s", "Invalid message size"); *err = 500; return 0; } if ((conn->request_len == 0) && (conn->data_len == conn->buf_size)) { mg_snprintf(conn, NULL, /* No truncation check for ebuf */ ebuf, ebuf_len, "%s", "Message too large"); *err = 413; return 0; } if (conn->request_len <= 0) { if (conn->data_len > 0) { mg_snprintf(conn, NULL, /* No truncation check for ebuf */ ebuf, ebuf_len, "%s", "Malformed message"); *err = 400; } else { /* Server did not recv anything -> just close the connection */ conn->must_close = 1; mg_snprintf(conn, NULL, /* No truncation check for ebuf */ ebuf, ebuf_len, "%s", "No data received"); *err = 0; } return 0; } return 1; } static int get_request(struct mg_connection *conn, char *ebuf, size_t ebuf_len, int *err) { const char *cl; conn->connection_type = CONNECTION_TYPE_REQUEST; /* request (valid of not) */ if (!get_message(conn, ebuf, ebuf_len, err)) { return 0; } if (parse_http_request(conn->buf, conn->buf_size, &conn->request_info) <= 0) { mg_snprintf(conn, NULL, /* No truncation check for ebuf */ ebuf, ebuf_len, "%s", "Bad request"); *err = 400; return 0; } /* Message is a valid request */ if (!switch_domain_context(conn)) { mg_snprintf(conn, NULL, /* No truncation check for ebuf */ ebuf, ebuf_len, "%s", "Bad request: Host mismatch"); *err = 400; return 0; } #if USE_ZLIB if (((cl = get_header(conn->request_info.http_headers, conn->request_info.num_headers, "Accept-Encoding")) != NULL) && strstr(cl, "gzip")) { conn->accept_gzip = 1; } #endif if (((cl = get_header(conn->request_info.http_headers, conn->request_info.num_headers, "Transfer-Encoding")) != NULL) && mg_strcasecmp(cl, "identity")) { if (mg_strcasecmp(cl, "chunked")) { mg_snprintf(conn, NULL, /* No truncation check for ebuf */ ebuf, ebuf_len, "%s", "Bad request"); *err = 400; return 0; } conn->is_chunked = 1; conn->content_len = 0; /* not yet read */ } else if ((cl = get_header(conn->request_info.http_headers, conn->request_info.num_headers, "Content-Length")) != NULL) { /* Request has content length set */ char *endptr = NULL; conn->content_len = strtoll(cl, &endptr, 10); if ((endptr == cl) || (conn->content_len < 0)) { mg_snprintf(conn, NULL, /* No truncation check for ebuf */ ebuf, ebuf_len, "%s", "Bad request"); *err = 411; return 0; } /* Publish the content length back to the request info. */ conn->request_info.content_length = conn->content_len; } else { /* There is no exception, see RFC7230. */ conn->content_len = 0; } return 1; } /* conn is assumed to be valid in this internal function */ static int get_response(struct mg_connection *conn, char *ebuf, size_t ebuf_len, int *err) { const char *cl; conn->connection_type = CONNECTION_TYPE_RESPONSE; /* response (valid or not) */ if (!get_message(conn, ebuf, ebuf_len, err)) { return 0; } if (parse_http_response(conn->buf, conn->buf_size, &conn->response_info) <= 0) { mg_snprintf(conn, NULL, /* No truncation check for ebuf */ ebuf, ebuf_len, "%s", "Bad response"); *err = 400; return 0; } /* Message is a valid response */ if (((cl = get_header(conn->response_info.http_headers, conn->response_info.num_headers, "Transfer-Encoding")) != NULL) && mg_strcasecmp(cl, "identity")) { if (mg_strcasecmp(cl, "chunked")) { mg_snprintf(conn, NULL, /* No truncation check for ebuf */ ebuf, ebuf_len, "%s", "Bad request"); *err = 400; return 0; } conn->is_chunked = 1; conn->content_len = 0; /* not yet read */ } else if ((cl = get_header(conn->response_info.http_headers, conn->response_info.num_headers, "Content-Length")) != NULL) { char *endptr = NULL; conn->content_len = strtoll(cl, &endptr, 10); if ((endptr == cl) || (conn->content_len < 0)) { mg_snprintf(conn, NULL, /* No truncation check for ebuf */ ebuf, ebuf_len, "%s", "Bad request"); *err = 411; return 0; } /* Publish the content length back to the response info. */ conn->response_info.content_length = conn->content_len; /* TODO: check if it is still used in response_info */ conn->request_info.content_length = conn->content_len; /* TODO: we should also consider HEAD method */ if (conn->response_info.status_code == 304) { conn->content_len = 0; } } else { /* TODO: we should also consider HEAD method */ if (((conn->response_info.status_code >= 100) && (conn->response_info.status_code <= 199)) || (conn->response_info.status_code == 204) || (conn->response_info.status_code == 304)) { conn->content_len = 0; } else { conn->content_len = -1; /* unknown content length */ } } return 1; } CIVETWEB_API int mg_get_response(struct mg_connection *conn, char *ebuf, size_t ebuf_len, int timeout) { int err, ret; char txt[32]; /* will not overflow */ char *save_timeout; char *new_timeout; if (ebuf_len > 0) { ebuf[0] = '\0'; } if (!conn) { mg_snprintf(conn, NULL, /* No truncation check for ebuf */ ebuf, ebuf_len, "%s", "Parameter error"); return -1; } /* Reset the previous responses */ conn->data_len = 0; /* Implementation of API function for HTTP clients */ save_timeout = conn->dom_ctx->config[REQUEST_TIMEOUT]; if (timeout >= 0) { mg_snprintf(conn, NULL, txt, sizeof(txt), "%i", timeout); new_timeout = txt; } else { new_timeout = NULL; } conn->dom_ctx->config[REQUEST_TIMEOUT] = new_timeout; ret = get_response(conn, ebuf, ebuf_len, &err); conn->dom_ctx->config[REQUEST_TIMEOUT] = save_timeout; /* TODO: here, the URI is the http response code */ conn->request_info.local_uri_raw = conn->request_info.request_uri; conn->request_info.local_uri = conn->request_info.local_uri_raw; /* TODO (mid): Define proper return values - maybe return length? * For the first test use <0 for error and >0 for OK */ return (ret == 0) ? -1 : +1; } CIVETWEB_API struct mg_connection * mg_download(const char *host, int port, int use_ssl, char *ebuf, size_t ebuf_len, const char *fmt, ...) { struct mg_connection *conn; va_list ap; int i; int reqerr; if (ebuf_len > 0) { ebuf[0] = '\0'; } va_start(ap, fmt); /* open a connection */ conn = mg_connect_client(host, port, use_ssl, ebuf, ebuf_len); if (conn != NULL) { i = mg_vprintf(conn, fmt, ap); if (i <= 0) { mg_snprintf(conn, NULL, /* No truncation check for ebuf */ ebuf, ebuf_len, "%s", "Error sending request"); } else { /* make sure the buffer is clear */ conn->data_len = 0; get_response(conn, ebuf, ebuf_len, &reqerr); /* TODO: here, the URI is the http response code */ conn->request_info.local_uri = conn->request_info.request_uri; } } /* if an error occurred, close the connection */ if ((ebuf[0] != '\0') && (conn != NULL)) { mg_close_connection(conn); conn = NULL; } va_end(ap); return conn; } struct websocket_client_thread_data { struct mg_connection *conn; mg_websocket_data_handler data_handler; mg_websocket_close_handler close_handler; void *callback_data; }; #if defined(USE_WEBSOCKET) #if defined(_WIN32) static unsigned __stdcall websocket_client_thread(void *data) #else static void * websocket_client_thread(void *data) #endif { struct websocket_client_thread_data *cdata = (struct websocket_client_thread_data *)data; void *user_thread_ptr = NULL; #if !defined(_WIN32) && !defined(__ZEPHYR__) struct sigaction sa; /* Ignore SIGPIPE */ memset(&sa, 0, sizeof(sa)); sa.sa_handler = SIG_IGN; sigaction(SIGPIPE, &sa, NULL); #endif mg_set_thread_name("ws-clnt"); if (cdata->conn->phys_ctx) { if (cdata->conn->phys_ctx->callbacks.init_thread) { /* 3 indicates a websocket client thread */ /* TODO: check if conn->phys_ctx can be set */ user_thread_ptr = cdata->conn->phys_ctx->callbacks.init_thread( cdata->conn->phys_ctx, 3); } } read_websocket(cdata->conn, cdata->data_handler, cdata->callback_data); DEBUG_TRACE("%s", "Websocket client thread exited\n"); if (cdata->close_handler != NULL) { cdata->close_handler(cdata->conn, cdata->callback_data); } /* The websocket_client context has only this thread. If it runs out, set the stop_flag to 2 (= "stopped"). */ STOP_FLAG_ASSIGN(&cdata->conn->phys_ctx->stop_flag, 2); if (cdata->conn->phys_ctx->callbacks.exit_thread) { cdata->conn->phys_ctx->callbacks.exit_thread(cdata->conn->phys_ctx, 3, user_thread_ptr); } mg_free((void *)cdata); #if defined(_WIN32) return 0; #else return NULL; #endif } #endif static struct mg_connection * mg_connect_websocket_client_impl(const struct mg_client_options *client_options, int use_ssl, char *error_buffer, size_t error_buffer_size, const char *path, const char *origin, const char *extensions, mg_websocket_data_handler data_func, mg_websocket_close_handler close_func, void *user_data) { struct mg_connection *conn = NULL; #if defined(USE_WEBSOCKET) struct websocket_client_thread_data *thread_data; static const char *magic = "x3JJHMbDL1EzLkh9GBhXDw=="; const char *host = client_options->host; int i; struct mg_init_data init; struct mg_error_data error; memset(&init, 0, sizeof(init)); memset(&error, 0, sizeof(error)); error.text_buffer_size = error_buffer_size; error.text = error_buffer; #if defined(__clang__) # pragma clang diagnostic push # pragma clang diagnostic ignored "-Wformat-nonliteral" #endif /* Establish the client connection and request upgrade */ conn = mg_connect_client_impl(client_options, use_ssl, &init, &error); /* Connection object will be null if something goes wrong */ if (conn == NULL) { /* error_buffer should be already filled ... */ if (!error_buffer[0]) { /* ... if not add an error message */ mg_snprintf(conn, NULL, /* No truncation check for ebuf */ error_buffer, error_buffer_size, "Unexpected error"); } return NULL; } if (origin != NULL) { if (extensions != NULL) { i = mg_printf(conn, "GET %s HTTP/1.1\r\n" "Host: %s\r\n" "Upgrade: websocket\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Key: %s\r\n" "Sec-WebSocket-Version: 13\r\n" "Sec-WebSocket-Extensions: %s\r\n" "Origin: %s\r\n" "\r\n", path, host, magic, extensions, origin); } else { i = mg_printf(conn, "GET %s HTTP/1.1\r\n" "Host: %s\r\n" "Upgrade: websocket\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Key: %s\r\n" "Sec-WebSocket-Version: 13\r\n" "Origin: %s\r\n" "\r\n", path, host, magic, origin); } } else { if (extensions != NULL) { i = mg_printf(conn, "GET %s HTTP/1.1\r\n" "Host: %s\r\n" "Upgrade: websocket\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Key: %s\r\n" "Sec-WebSocket-Version: 13\r\n" "Sec-WebSocket-Extensions: %s\r\n" "\r\n", path, host, magic, extensions); } else { i = mg_printf(conn, "GET %s HTTP/1.1\r\n" "Host: %s\r\n" "Upgrade: websocket\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Key: %s\r\n" "Sec-WebSocket-Version: 13\r\n" "\r\n", path, host, magic); } } if (i <= 0) { mg_snprintf(conn, NULL, /* No truncation check for ebuf */ error_buffer, error_buffer_size, "%s", "Error sending request"); mg_close_connection(conn); return NULL; } conn->data_len = 0; if (!get_response(conn, error_buffer, error_buffer_size, &i)) { mg_close_connection(conn); return NULL; } conn->request_info.local_uri_raw = conn->request_info.request_uri; conn->request_info.local_uri = conn->request_info.local_uri_raw; #if defined(__clang__) # pragma clang diagnostic pop #endif if (conn->response_info.status_code != 101) { /* We sent an "upgrade" request. For a correct websocket * protocol handshake, we expect a "101 Continue" response. * Otherwise it is a protocol violation. Maybe the HTTP * Server does not know websockets. */ if (!*error_buffer) { /* set an error, if not yet set */ mg_snprintf(conn, NULL, /* No truncation check for ebuf */ error_buffer, error_buffer_size, "Unexpected server reply"); } DEBUG_TRACE("Websocket client connect error: %s\r\n", error_buffer); mg_close_connection(conn); return NULL; } thread_data = (struct websocket_client_thread_data *)mg_calloc_ctx( 1, sizeof(struct websocket_client_thread_data), conn->phys_ctx); if (!thread_data) { DEBUG_TRACE("%s\r\n", "Out of memory"); mg_close_connection(conn); return NULL; } thread_data->conn = conn; thread_data->data_handler = data_func; thread_data->close_handler = close_func; thread_data->callback_data = user_data; conn->phys_ctx->worker_threadids = (pthread_t *)mg_calloc_ctx(1, sizeof(pthread_t), conn->phys_ctx); if (!conn->phys_ctx->worker_threadids) { DEBUG_TRACE("%s\r\n", "Out of memory"); mg_free(thread_data); mg_close_connection(conn); return NULL; } /* Now upgrade to ws/wss client context */ conn->phys_ctx->user_data = user_data; conn->phys_ctx->context_type = CONTEXT_WS_CLIENT; conn->phys_ctx->cfg_worker_threads = 1; /* one worker thread */ /* Start a thread to read the websocket client connection * This thread will automatically stop when mg_disconnect is * called on the client connection */ if (mg_start_thread_with_id(websocket_client_thread, thread_data, conn->phys_ctx->worker_threadids) != 0) { conn->phys_ctx->cfg_worker_threads = 0; mg_free(thread_data); mg_close_connection(conn); conn = NULL; DEBUG_TRACE("%s", "Websocket client connect thread could not be started\r\n"); } #else /* Appease "unused parameter" warnings */ (void)client_options; (void)use_ssl; (void)error_buffer; (void)error_buffer_size; (void)path; (void)origin; (void)extensions; (void)user_data; (void)data_func; (void)close_func; #endif return conn; } CIVETWEB_API struct mg_connection * mg_connect_websocket_client(const char *host, int port, int use_ssl, char *error_buffer, size_t error_buffer_size, const char *path, const char *origin, mg_websocket_data_handler data_func, mg_websocket_close_handler close_func, void *user_data) { struct mg_client_options client_options; memset(&client_options, 0, sizeof(client_options)); client_options.host = host; client_options.port = port; return mg_connect_websocket_client_impl(&client_options, use_ssl, error_buffer, error_buffer_size, path, origin, NULL, data_func, close_func, user_data); } CIVETWEB_API struct mg_connection * mg_connect_websocket_client_secure( const struct mg_client_options *client_options, char *error_buffer, size_t error_buffer_size, const char *path, const char *origin, mg_websocket_data_handler data_func, mg_websocket_close_handler close_func, void *user_data) { if (!client_options) { return NULL; } return mg_connect_websocket_client_impl(client_options, 1, error_buffer, error_buffer_size, path, origin, NULL, data_func, close_func, user_data); } CIVETWEB_API struct mg_connection * mg_connect_websocket_client_extensions(const char *host, int port, int use_ssl, char *error_buffer, size_t error_buffer_size, const char *path, const char *origin, const char *extensions, mg_websocket_data_handler data_func, mg_websocket_close_handler close_func, void *user_data) { struct mg_client_options client_options; memset(&client_options, 0, sizeof(client_options)); client_options.host = host; client_options.port = port; return mg_connect_websocket_client_impl(&client_options, use_ssl, error_buffer, error_buffer_size, path, origin, extensions, data_func, close_func, user_data); } CIVETWEB_API struct mg_connection * mg_connect_websocket_client_secure_extensions( const struct mg_client_options *client_options, char *error_buffer, size_t error_buffer_size, const char *path, const char *origin, const char *extensions, mg_websocket_data_handler data_func, mg_websocket_close_handler close_func, void *user_data) { if (!client_options) { return NULL; } return mg_connect_websocket_client_impl(client_options, 1, error_buffer, error_buffer_size, path, origin, extensions, data_func, close_func, user_data); } /* Prepare connection data structure */ static void init_connection(struct mg_connection *conn) { /* Is keep alive allowed by the server */ int keep_alive_enabled = !mg_strcasecmp(conn->dom_ctx->config[ENABLE_KEEP_ALIVE], "yes"); if (!keep_alive_enabled) { conn->must_close = 1; } /* Important: on new connection, reset the receiving buffer. Credit * goes to crule42. */ conn->data_len = 0; conn->handled_requests = 0; conn->connection_type = CONNECTION_TYPE_INVALID; conn->request_info.acceptedWebSocketSubprotocol = NULL; mg_set_user_connection_data(conn, NULL); #if defined(USE_SERVER_STATS) conn->conn_state = 2; /* init */ #endif /* call the init_connection callback if assigned */ if (conn->phys_ctx->callbacks.init_connection != NULL) { if (conn->phys_ctx->context_type == CONTEXT_SERVER) { void *conn_data = NULL; conn->phys_ctx->callbacks.init_connection(conn, &conn_data); mg_set_user_connection_data(conn, conn_data); } } } /* Process a connection - may handle multiple requests * using the same connection. * Must be called with a valid connection (conn and * conn->phys_ctx must be valid). */ static void process_new_connection(struct mg_connection *conn) { struct mg_request_info *ri = &conn->request_info; int keep_alive, discard_len; char ebuf[100]; const char *hostend; int reqerr, uri_type; #if defined(USE_SERVER_STATS) ptrdiff_t mcon = mg_atomic_inc(&(conn->phys_ctx->active_connections)); mg_atomic_add(&(conn->phys_ctx->total_connections), 1); mg_atomic_max(&(conn->phys_ctx->max_active_connections), mcon); #endif DEBUG_TRACE("Start processing connection from %s", conn->request_info.remote_addr); /* Loop over multiple requests sent using the same connection * (while "keep alive"). */ do { DEBUG_TRACE("calling get_request (%i times for this connection)", conn->handled_requests + 1); #if defined(USE_SERVER_STATS) conn->conn_state = 3; /* ready */ #endif if (!get_request(conn, ebuf, sizeof(ebuf), &reqerr)) { /* The request sent by the client could not be understood by * the server, or it was incomplete or a timeout. Send an * error message and close the connection. */ if (reqerr > 0) { DEBUG_ASSERT(ebuf[0] != '\0'); mg_send_http_error(conn, reqerr, "%s", ebuf); } } else if (strcmp(ri->http_version, "1.0") && strcmp(ri->http_version, "1.1")) { /* HTTP/2 is not allowed here */ mg_snprintf(conn, NULL, /* No truncation check for ebuf */ ebuf, sizeof(ebuf), "Bad HTTP version: [%s]", ri->http_version); mg_send_http_error(conn, 505, "%s", ebuf); } if (ebuf[0] == '\0') { uri_type = get_uri_type(conn->request_info.request_uri); switch (uri_type) { case 1: /* Asterisk */ conn->request_info.local_uri_raw = 0; /* TODO: Deal with '*'. */ break; case 2: /* relative uri */ conn->request_info.local_uri_raw = conn->request_info.request_uri; break; case 3: case 4: /* absolute uri (with/without port) */ hostend = get_rel_url_at_current_server( conn->request_info.request_uri, conn); if (hostend) { conn->request_info.local_uri_raw = hostend; } else { conn->request_info.local_uri_raw = NULL; } break; default: mg_snprintf(conn, NULL, /* No truncation check for ebuf */ ebuf, sizeof(ebuf), "Invalid URI"); mg_send_http_error(conn, 400, "%s", ebuf); conn->request_info.local_uri_raw = NULL; break; } conn->request_info.local_uri = (char *)conn->request_info.local_uri_raw; } if (ebuf[0] != '\0') { conn->protocol_type = -1; } else { /* HTTP/1 allows protocol upgrade */ conn->protocol_type = should_switch_to_protocol(conn); if (conn->protocol_type == PROTOCOL_TYPE_HTTP2) { /* This will occur, if a HTTP/1.1 request should be upgraded * to HTTP/2 - but not if HTTP/2 is negotiated using ALPN. * Since most (all?) major browsers only support HTTP/2 using * ALPN, this is hard to test and very low priority. * Deactivate it (at least for now). */ conn->protocol_type = PROTOCOL_TYPE_HTTP1; } } DEBUG_TRACE("http: %s, error: %s", (ri->http_version ? ri->http_version : "none"), (ebuf[0] ? ebuf : "none")); if (ebuf[0] == '\0') { if (conn->request_info.local_uri) { /* handle request to local server */ handle_request_stat_log(conn); } else { /* TODO: handle non-local request (PROXY) */ conn->must_close = 1; } } else { conn->must_close = 1; } /* Response complete. Free header buffer */ free_buffered_response_header_list(conn); if (ri->remote_user != NULL) { mg_free((void *)ri->remote_user); /* Important! When having connections with and without auth * would cause double free and then crash */ ri->remote_user = NULL; } /* NOTE(lsm): order is important here. should_keep_alive() call * is using parsed request, which will be invalid after * memmove's below. * Therefore, memorize should_keep_alive() result now for later * use in loop exit condition. */ /* Enable it only if this request is completely discardable. */ keep_alive = STOP_FLAG_IS_ZERO(&conn->phys_ctx->stop_flag) && should_keep_alive(conn) && (conn->content_len >= 0) && (conn->request_len > 0) && ((conn->is_chunked == 4) || (!conn->is_chunked && ((conn->consumed_content == conn->content_len) || ((conn->request_len + conn->content_len) <= conn->data_len)))) && (conn->protocol_type == PROTOCOL_TYPE_HTTP1); if (keep_alive) { /* Discard all buffered data for this request */ discard_len = ((conn->request_len + conn->content_len) < conn->data_len) ? (int)(conn->request_len + conn->content_len) : conn->data_len; conn->data_len -= discard_len; if (conn->data_len > 0) { DEBUG_TRACE("discard_len = %d", discard_len); memmove(conn->buf, conn->buf + discard_len, (size_t)conn->data_len); } } DEBUG_ASSERT(conn->data_len >= 0); DEBUG_ASSERT(conn->data_len <= conn->buf_size); if ((conn->data_len < 0) || (conn->data_len > conn->buf_size)) { DEBUG_TRACE("internal error: data_len = %li, buf_size = %li", (long int)conn->data_len, (long int)conn->buf_size); break; } conn->handled_requests++; } while (keep_alive); DEBUG_TRACE("Done processing connection from %s (%f sec)", conn->request_info.remote_addr, difftime(time(NULL), conn->conn_birth_time)); close_connection(conn); #if defined(USE_SERVER_STATS) mg_atomic_add(&(conn->phys_ctx->total_requests), conn->handled_requests); mg_atomic_dec(&(conn->phys_ctx->active_connections)); #endif } #if defined(ALTERNATIVE_QUEUE) static void produce_socket(struct mg_context *ctx, const struct socket *sp) { unsigned int i; while (!ctx->stop_flag) { for (i = 0; i < ctx->cfg_worker_threads; i++) { /* find a free worker slot and signal it */ if (ctx->client_socks[i].in_use == 2) { (void)pthread_mutex_lock(&ctx->thread_mutex); if ((ctx->client_socks[i].in_use == 2) && !ctx->stop_flag) { ctx->client_socks[i] = *sp; ctx->client_socks[i].in_use = 1; /* socket has been moved to the consumer */ (void)pthread_mutex_unlock(&ctx->thread_mutex); (void)event_signal(ctx->client_wait_events[i]); return; } (void)pthread_mutex_unlock(&ctx->thread_mutex); } } /* queue is full */ mg_sleep(1); } /* must consume */ set_blocking_mode(sp->sock); closesocket(sp->sock); } static int consume_socket(struct mg_context *ctx, struct socket *sp, int thread_index) { DEBUG_TRACE("%s", "going idle"); (void)pthread_mutex_lock(&ctx->thread_mutex); ctx->client_socks[thread_index].in_use = 2; (void)pthread_mutex_unlock(&ctx->thread_mutex); event_wait(ctx->client_wait_events[thread_index]); (void)pthread_mutex_lock(&ctx->thread_mutex); *sp = ctx->client_socks[thread_index]; if (ctx->stop_flag) { (void)pthread_mutex_unlock(&ctx->thread_mutex); if (sp->in_use == 1) { /* must consume */ set_blocking_mode(sp->sock); closesocket(sp->sock); } return 0; } (void)pthread_mutex_unlock(&ctx->thread_mutex); if (sp->in_use == 1) { DEBUG_TRACE("grabbed socket %d, going busy", sp->sock); return 1; } /* must not reach here */ DEBUG_ASSERT(0); return 0; } #else /* ALTERNATIVE_QUEUE */ /* Worker threads take accepted socket from the queue */ static int consume_socket(struct mg_context *ctx, struct socket *sp, int thread_index) { (void)thread_index; (void)pthread_mutex_lock(&ctx->thread_mutex); DEBUG_TRACE("%s", "going idle"); /* If the queue is empty, wait. We're idle at this point. */ while ((ctx->sq_head == ctx->sq_tail) && (STOP_FLAG_IS_ZERO(&ctx->stop_flag))) { pthread_cond_wait(&ctx->sq_full, &ctx->thread_mutex); } /* If we're stopping, sq_head may be equal to sq_tail. */ if (ctx->sq_head > ctx->sq_tail) { /* Copy socket from the queue and increment tail */ *sp = ctx->squeue[ctx->sq_tail % ctx->sq_size]; ctx->sq_tail++; DEBUG_TRACE("grabbed socket %d, going busy", sp ? sp->sock : -1); /* Wrap pointers if needed */ while (ctx->sq_tail > ctx->sq_size) { ctx->sq_tail -= ctx->sq_size; ctx->sq_head -= ctx->sq_size; } } (void)pthread_cond_signal(&ctx->sq_empty); (void)pthread_mutex_unlock(&ctx->thread_mutex); return STOP_FLAG_IS_ZERO(&ctx->stop_flag); } /* Master thread adds accepted socket to a queue */ static void produce_socket(struct mg_context *ctx, const struct socket *sp) { int queue_filled; (void)pthread_mutex_lock(&ctx->thread_mutex); queue_filled = ctx->sq_head - ctx->sq_tail; /* If the queue is full, wait */ while (STOP_FLAG_IS_ZERO(&ctx->stop_flag) && (queue_filled >= ctx->sq_size)) { ctx->sq_blocked = 1; /* Status information: All threads busy */ #if defined(USE_SERVER_STATS) if (queue_filled > ctx->sq_max_fill) { ctx->sq_max_fill = queue_filled; } #endif (void)pthread_cond_wait(&ctx->sq_empty, &ctx->thread_mutex); ctx->sq_blocked = 0; /* Not blocked now */ queue_filled = ctx->sq_head - ctx->sq_tail; } if (queue_filled < ctx->sq_size) { /* Copy socket to the queue and increment head */ ctx->squeue[ctx->sq_head % ctx->sq_size] = *sp; ctx->sq_head++; DEBUG_TRACE("queued socket %d", sp ? sp->sock : -1); } queue_filled = ctx->sq_head - ctx->sq_tail; #if defined(USE_SERVER_STATS) if (queue_filled > ctx->sq_max_fill) { ctx->sq_max_fill = queue_filled; } #endif (void)pthread_cond_signal(&ctx->sq_full); (void)pthread_mutex_unlock(&ctx->thread_mutex); } #endif /* ALTERNATIVE_QUEUE */ static void worker_thread_run(struct mg_connection *conn) { struct mg_context *ctx = conn->phys_ctx; int thread_index; struct mg_workerTLS tls; mg_set_thread_name("worker"); tls.is_master = 0; tls.thread_idx = (unsigned)mg_atomic_inc(&thread_idx_max); #if defined(_WIN32) tls.pthread_cond_helper_mutex = CreateEvent(NULL, FALSE, FALSE, NULL); #endif /* Initialize thread local storage before calling any callback */ pthread_setspecific(sTlsKey, &tls); /* Check if there is a user callback */ if (ctx->callbacks.init_thread) { /* call init_thread for a worker thread (type 1), and store the * return value */ tls.user_ptr = ctx->callbacks.init_thread(ctx, 1); } else { /* No callback: set user pointer to NULL */ tls.user_ptr = NULL; } /* Connection structure has been pre-allocated */ thread_index = (int)(conn - ctx->worker_connections); if ((thread_index < 0) || ((unsigned)thread_index >= (unsigned)ctx->cfg_worker_threads)) { mg_cry_ctx_internal(ctx, "Internal error: Invalid worker index %i", thread_index); return; } /* Request buffers are not pre-allocated. They are private to the * request and do not contain any state information that might be * of interest to anyone observing a server status. */ conn->buf = (char *)mg_malloc_ctx(ctx->max_request_size, conn->phys_ctx); if (conn->buf == NULL) { mg_cry_ctx_internal( ctx, "Out of memory: Cannot allocate buffer for worker %i", thread_index); return; } conn->buf_size = (int)ctx->max_request_size; conn->dom_ctx = &(ctx->dd); /* Use default domain and default host */ conn->tls_user_ptr = tls.user_ptr; /* store ptr for quick access */ conn->request_info.user_data = ctx->user_data; /* Allocate a mutex for this connection to allow communication both * within the request handler and from elsewhere in the application */ if (0 != pthread_mutex_init(&conn->mutex, &pthread_mutex_attr)) { mg_free(conn->buf); mg_cry_ctx_internal(ctx, "%s", "Cannot create mutex"); return; } #if defined(USE_SERVER_STATS) conn->conn_state = 1; /* not consumed */ #endif /* Call consume_socket() even when ctx->stop_flag > 0, to let it * signal sq_empty condvar to wake up the master waiting in * produce_socket() */ while (consume_socket(ctx, &conn->client, thread_index)) { /* New connections must start with new protocol negotiation */ tls.alpn_proto = NULL; #if defined(USE_SERVER_STATS) conn->conn_close_time = 0; #endif conn->conn_birth_time = time(NULL); /* Fill in IP, port info early so even if SSL setup below fails, * error handler would have the corresponding info. * Thanks to Johannes Winkelmann for the patch. */ conn->request_info.remote_port = ntohs(USA_IN_PORT_UNSAFE(&conn->client.rsa)); conn->request_info.server_port = ntohs(USA_IN_PORT_UNSAFE(&conn->client.lsa)); sockaddr_to_string(conn->request_info.remote_addr, sizeof(conn->request_info.remote_addr), &conn->client.rsa); DEBUG_TRACE("Incoming %sconnection from %s", (conn->client.is_ssl ? "SSL " : ""), conn->request_info.remote_addr); conn->request_info.is_ssl = conn->client.is_ssl; if (conn->client.is_ssl) { #if defined(USE_MBEDTLS) /* HTTPS connection */ if (mbed_ssl_accept(&(conn->ssl), conn->dom_ctx->ssl_ctx, (int *)&(conn->client.sock), conn->phys_ctx) == 0) { /* conn->dom_ctx is set in get_request */ /* process HTTPS connection */ init_connection(conn); conn->connection_type = CONNECTION_TYPE_REQUEST; conn->protocol_type = PROTOCOL_TYPE_HTTP1; process_new_connection(conn); } else { /* make sure the connection is cleaned up on SSL failure */ close_connection(conn); } #elif !defined(NO_SSL) /* HTTPS connection */ if (sslize(conn, SSL_accept, NULL)) { /* conn->dom_ctx is set in get_request */ /* Get SSL client certificate information (if set) */ struct mg_client_cert client_cert; if (ssl_get_client_cert_info(conn, &client_cert)) { conn->request_info.client_cert = &client_cert; } /* process HTTPS connection */ #if defined(USE_HTTP2) if ((tls.alpn_proto != NULL) && (!memcmp(tls.alpn_proto, "\x02h2", 3))) { /* process HTTPS/2 connection */ init_connection(conn); conn->connection_type = CONNECTION_TYPE_REQUEST; conn->protocol_type = PROTOCOL_TYPE_HTTP2; conn->content_len = -1; /* content length is not predefined */ conn->is_chunked = 0; /* HTTP2 is never chunked */ process_new_http2_connection(conn); } else #endif { /* process HTTPS/1.x or WEBSOCKET-SECURE connection */ init_connection(conn); conn->connection_type = CONNECTION_TYPE_REQUEST; /* Start with HTTP, WS will be an "upgrade" request later */ conn->protocol_type = PROTOCOL_TYPE_HTTP1; process_new_connection(conn); } /* Free client certificate info */ if (conn->request_info.client_cert) { mg_free((void *)(conn->request_info.client_cert->subject)); mg_free((void *)(conn->request_info.client_cert->issuer)); mg_free((void *)(conn->request_info.client_cert->serial)); mg_free((void *)(conn->request_info.client_cert->finger)); /* Free certificate memory */ X509_free( (X509 *)conn->request_info.client_cert->peer_cert); conn->request_info.client_cert->peer_cert = 0; conn->request_info.client_cert->subject = 0; conn->request_info.client_cert->issuer = 0; conn->request_info.client_cert->serial = 0; conn->request_info.client_cert->finger = 0; conn->request_info.client_cert = 0; } } else { /* make sure the connection is cleaned up on SSL failure */ close_connection(conn); } #endif } else { /* process HTTP connection */ init_connection(conn); conn->connection_type = CONNECTION_TYPE_REQUEST; /* Start with HTTP, WS will be an "upgrade" request later */ conn->protocol_type = PROTOCOL_TYPE_HTTP1; process_new_connection(conn); } DEBUG_TRACE("%s", "Connection closed"); #if defined(USE_SERVER_STATS) conn->conn_close_time = time(NULL); #endif } /* Call exit thread user callback */ if (ctx->callbacks.exit_thread) { ctx->callbacks.exit_thread(ctx, 1, tls.user_ptr); } /* delete thread local storage objects */ pthread_setspecific(sTlsKey, NULL); #if defined(_WIN32) CloseHandle(tls.pthread_cond_helper_mutex); #endif pthread_mutex_destroy(&conn->mutex); /* Free the request buffer. */ conn->buf_size = 0; mg_free(conn->buf); conn->buf = NULL; /* Free cleaned URI (if any) */ if (conn->request_info.local_uri != conn->request_info.local_uri_raw) { mg_free((void *)conn->request_info.local_uri); conn->request_info.local_uri = NULL; } #if defined(USE_SERVER_STATS) conn->conn_state = 9; /* done */ #endif DEBUG_TRACE("%s", "exiting"); } /* Threads have different return types on Windows and Unix. */ #if defined(_WIN32) static unsigned __stdcall worker_thread(void *thread_func_param) { worker_thread_run((struct mg_connection *)thread_func_param); return 0; } #else static void * worker_thread(void *thread_func_param) { #if !defined(__ZEPHYR__) struct sigaction sa; /* Ignore SIGPIPE */ memset(&sa, 0, sizeof(sa)); sa.sa_handler = SIG_IGN; sigaction(SIGPIPE, &sa, NULL); #endif worker_thread_run((struct mg_connection *)thread_func_param); return NULL; } #endif /* _WIN32 */ /* This is an internal function, thus all arguments are expected to be * valid - a NULL check is not required. */ static void accept_new_connection(const struct socket *listener, struct mg_context *ctx) { struct socket so; char src_addr[IP_ADDR_STR_LEN]; socklen_t len = sizeof(so.rsa); #if !defined(__ZEPHYR__) int on = 1; #endif memset(&so, 0, sizeof(so)); if ((so.sock = accept(listener->sock, &so.rsa.sa, &len)) == INVALID_SOCKET) { } else if (check_acl(ctx, &so.rsa) != 1) { sockaddr_to_string(src_addr, sizeof(src_addr), &so.rsa); mg_cry_ctx_internal(ctx, "%s: %s is not allowed to connect", __func__, src_addr); closesocket(so.sock); } else { /* Put so socket structure into the queue */ DEBUG_TRACE("Accepted socket %d", (int)so.sock); set_close_on_exec(so.sock, NULL, ctx); so.is_ssl = listener->is_ssl; so.ssl_redir = listener->ssl_redir; if (getsockname(so.sock, &so.lsa.sa, &len) != 0) { mg_cry_ctx_internal(ctx, "%s: getsockname() failed: %s", __func__, strerror(ERRNO)); } #if !defined(__ZEPHYR__) if ((so.lsa.sa.sa_family == AF_INET) || (so.lsa.sa.sa_family == AF_INET6)) { /* Set TCP keep-alive for TCP sockets (IPv4 and IPv6). * This is needed because if HTTP-level keep-alive * is enabled, and client resets the connection, server won't get * TCP FIN or RST and will keep the connection open forever. With * TCP keep-alive, next keep-alive handshake will figure out that * the client is down and will close the server end. * Thanks to Igor Klopov who suggested the patch. */ if (setsockopt(so.sock, SOL_SOCKET, SO_KEEPALIVE, (SOCK_OPT_TYPE)&on, sizeof(on)) != 0) { mg_cry_ctx_internal( ctx, "%s: setsockopt(SOL_SOCKET SO_KEEPALIVE) failed: %s", __func__, strerror(ERRNO)); } } #endif /* Disable TCP Nagle's algorithm. Normally TCP packets are coalesced * to effectively fill up the underlying IP packet payload and * reduce the overhead of sending lots of small buffers. However * this hurts the server's throughput (ie. operations per second) * when HTTP 1.1 persistent connections are used and the responses * are relatively small (eg. less than 1400 bytes). */ if ((ctx->dd.config[CONFIG_TCP_NODELAY] != NULL) && (!strcmp(ctx->dd.config[CONFIG_TCP_NODELAY], "1"))) { if (set_tcp_nodelay(&so, 1) != 0) { mg_cry_ctx_internal( ctx, "%s: setsockopt(IPPROTO_TCP TCP_NODELAY) failed: %s", __func__, strerror(ERRNO)); } } /* The "non blocking" property should already be * inherited from the parent socket. Set it for * non-compliant socket implementations. */ set_non_blocking_mode(so.sock); so.in_use = 0; produce_socket(ctx, &so); } } static void master_thread_run(struct mg_context *ctx) { struct mg_workerTLS tls; struct mg_pollfd *pfd; unsigned int i; unsigned int workerthreadcount; if (!ctx) { return; } mg_set_thread_name("master"); /* Increase priority of the master thread */ #if defined(_WIN32) SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_ABOVE_NORMAL); #elif defined(USE_MASTER_THREAD_PRIORITY) int min_prio = sched_get_priority_min(SCHED_RR); int max_prio = sched_get_priority_max(SCHED_RR); if ((min_prio >= 0) && (max_prio >= 0) && ((USE_MASTER_THREAD_PRIORITY) <= max_prio) && ((USE_MASTER_THREAD_PRIORITY) >= min_prio)) { struct sched_param sched_param = {0}; sched_param.sched_priority = (USE_MASTER_THREAD_PRIORITY); pthread_setschedparam(pthread_self(), SCHED_RR, &sched_param); } #endif /* Initialize thread local storage */ #if defined(_WIN32) tls.pthread_cond_helper_mutex = CreateEvent(NULL, FALSE, FALSE, NULL); #endif tls.is_master = 1; pthread_setspecific(sTlsKey, &tls); if (ctx->callbacks.init_thread) { /* Callback for the master thread (type 0) */ tls.user_ptr = ctx->callbacks.init_thread(ctx, 0); } else { tls.user_ptr = NULL; } /* Lua background script "start" event */ #if defined(USE_LUA) if (ctx->lua_background_state) { lua_State *lstate = (lua_State *)ctx->lua_background_state; pthread_mutex_lock(&ctx->lua_bg_mutex); /* call "start()" in Lua */ lua_getglobal(lstate, "start"); if (lua_type(lstate, -1) == LUA_TFUNCTION) { int ret = lua_pcall(lstate, /* args */ 0, /* results */ 0, 0); if (ret != 0) { struct mg_connection fc; lua_cry(fake_connection(&fc, ctx), ret, lstate, "lua_background_script", "start"); } } else { lua_pop(lstate, 1); } /* determine if there is a "log()" function in Lua background script */ lua_getglobal(lstate, "log"); if (lua_type(lstate, -1) == LUA_TFUNCTION) { ctx->lua_bg_log_available = 1; } lua_pop(lstate, 1); pthread_mutex_unlock(&ctx->lua_bg_mutex); } #endif /* Server starts *now* */ ctx->start_time = time(NULL); /* Server accept loop */ pfd = ctx->listening_socket_fds; while (STOP_FLAG_IS_ZERO(&ctx->stop_flag)) { for (i = 0; i < ctx->num_listening_sockets; i++) { pfd[i].fd = ctx->listening_sockets[i].sock; pfd[i].events = POLLIN; } if (mg_poll(pfd, ctx->num_listening_sockets, SOCKET_TIMEOUT_QUANTUM, &(ctx->stop_flag)) > 0) { for (i = 0; i < ctx->num_listening_sockets; i++) { /* NOTE(lsm): on QNX, poll() returns POLLRDNORM after the * successful poll, and POLLIN is defined as * (POLLRDNORM | POLLRDBAND) * Therefore, we're checking pfd[i].revents & POLLIN, not * pfd[i].revents == POLLIN. */ if (STOP_FLAG_IS_ZERO(&ctx->stop_flag) && (pfd[i].revents & POLLIN)) { accept_new_connection(&ctx->listening_sockets[i], ctx); } } } } /* Here stop_flag is 1 - Initiate shutdown. */ DEBUG_TRACE("%s", "stopping workers"); /* Stop signal received: somebody called mg_stop. Quit. */ close_all_listening_sockets(ctx); /* Wakeup workers that are waiting for connections to handle. */ #if defined(ALTERNATIVE_QUEUE) for (i = 0; i < ctx->cfg_worker_threads; i++) { event_signal(ctx->client_wait_events[i]); } #else (void)pthread_mutex_lock(&ctx->thread_mutex); pthread_cond_broadcast(&ctx->sq_full); (void)pthread_mutex_unlock(&ctx->thread_mutex); #endif /* Join all worker threads to avoid leaking threads. */ workerthreadcount = ctx->cfg_worker_threads; for (i = 0; i < workerthreadcount; i++) { if (ctx->worker_threadids[i] != 0) { mg_join_thread(ctx->worker_threadids[i]); } } #if defined(USE_LUA) /* Free Lua state of lua background task */ if (ctx->lua_background_state) { lua_State *lstate = (lua_State *)ctx->lua_background_state; ctx->lua_bg_log_available = 0; /* call "stop()" in Lua */ pthread_mutex_lock(&ctx->lua_bg_mutex); lua_getglobal(lstate, "stop"); if (lua_type(lstate, -1) == LUA_TFUNCTION) { int ret = lua_pcall(lstate, /* args */ 0, /* results */ 0, 0); if (ret != 0) { struct mg_connection fc; lua_cry(fake_connection(&fc, ctx), ret, lstate, "lua_background_script", "stop"); } } DEBUG_TRACE("Close Lua background state %p", lstate); lua_close(lstate); ctx->lua_background_state = 0; pthread_mutex_unlock(&ctx->lua_bg_mutex); } #endif DEBUG_TRACE("%s", "exiting"); /* call exit thread callback */ if (ctx->callbacks.exit_thread) { /* Callback for the master thread (type 0) */ ctx->callbacks.exit_thread(ctx, 0, tls.user_ptr); } #if defined(_WIN32) CloseHandle(tls.pthread_cond_helper_mutex); #endif pthread_setspecific(sTlsKey, NULL); /* Signal mg_stop() that we're done. * WARNING: This must be the very last thing this * thread does, as ctx becomes invalid after this line. */ STOP_FLAG_ASSIGN(&ctx->stop_flag, 2); } /* Threads have different return types on Windows and Unix. */ #if defined(_WIN32) static unsigned __stdcall master_thread(void *thread_func_param) { master_thread_run((struct mg_context *)thread_func_param); return 0; } #else static void * master_thread(void *thread_func_param) { #if !defined(__ZEPHYR__) struct sigaction sa; /* Ignore SIGPIPE */ memset(&sa, 0, sizeof(sa)); sa.sa_handler = SIG_IGN; sigaction(SIGPIPE, &sa, NULL); #endif master_thread_run((struct mg_context *)thread_func_param); return NULL; } #endif /* _WIN32 */ static void free_context(struct mg_context *ctx) { int i; struct mg_handler_info *tmp_rh; if (ctx == NULL) { return; } /* Call user callback */ if (ctx->callbacks.exit_context) { ctx->callbacks.exit_context(ctx); } /* All threads exited, no sync is needed. Destroy thread mutex and * condvars */ (void)pthread_mutex_destroy(&ctx->thread_mutex); #if defined(ALTERNATIVE_QUEUE) mg_free(ctx->client_socks); if (ctx->client_wait_events != NULL) { for (i = 0; (unsigned)i < ctx->cfg_worker_threads; i++) { event_destroy(ctx->client_wait_events[i]); } mg_free(ctx->client_wait_events); } #else (void)pthread_cond_destroy(&ctx->sq_empty); (void)pthread_cond_destroy(&ctx->sq_full); mg_free(ctx->squeue); #endif /* Destroy other context global data structures mutex */ (void)pthread_mutex_destroy(&ctx->nonce_mutex); #if defined(USE_LUA) (void)pthread_mutex_destroy(&ctx->lua_bg_mutex); #endif /* Deallocate config parameters */ for (i = 0; i < NUM_OPTIONS; i++) { if (ctx->dd.config[i] != NULL) { #if defined(_MSC_VER) # pragma warning(suppress : 6001) #endif mg_free(ctx->dd.config[i]); } } /* Deallocate request handlers */ while (ctx->dd.handlers) { tmp_rh = ctx->dd.handlers; ctx->dd.handlers = tmp_rh->next; mg_free(tmp_rh->uri); mg_free(tmp_rh); } #if defined(USE_MBEDTLS) if (ctx->dd.ssl_ctx != NULL) { mbed_sslctx_uninit(ctx->dd.ssl_ctx); mg_free(ctx->dd.ssl_ctx); ctx->dd.ssl_ctx = NULL; } #elif !defined(NO_SSL) /* Deallocate SSL context */ if (ctx->dd.ssl_ctx != NULL) { void *ssl_ctx = (void *)ctx->dd.ssl_ctx; int callback_ret = (ctx->callbacks.external_ssl_ctx == NULL) ? 0 : (ctx->callbacks.external_ssl_ctx(&ssl_ctx, ctx->user_data)); if (callback_ret == 0) { SSL_CTX_free(ctx->dd.ssl_ctx); } /* else: ignore error and omit SSL_CTX_free in case * callback_ret is 1 */ } #endif /* !NO_SSL */ /* Deallocate worker thread ID array */ mg_free(ctx->worker_threadids); /* Deallocate worker thread ID array */ mg_free(ctx->worker_connections); /* deallocate system name string */ mg_free(ctx->systemName); /* Deallocate context itself */ mg_free(ctx); } CIVETWEB_API void mg_stop(struct mg_context *ctx) { pthread_t mt; if (!ctx) { return; } /* We don't use a lock here. Calling mg_stop with the same ctx from * two threads is not allowed. */ mt = ctx->masterthreadid; if (mt == 0) { return; } ctx->masterthreadid = 0; /* Set stop flag, so all threads know they have to exit. */ STOP_FLAG_ASSIGN(&ctx->stop_flag, 1); /* Join timer thread */ #if defined(USE_TIMERS) timers_exit(ctx); #endif /* Wait until everything has stopped. */ while (!STOP_FLAG_IS_TWO(&ctx->stop_flag)) { (void)mg_sleep(10); } /* Wait to stop master thread */ mg_join_thread(mt); /* Close remaining Lua states */ #if defined(USE_LUA) lua_ctx_exit(ctx); #endif /* Free memory */ free_context(ctx); } static void get_system_name(char **sysName) { #if defined(_WIN32) char name[128]; DWORD dwVersion = 0; DWORD dwMajorVersion = 0; DWORD dwMinorVersion = 0; DWORD dwBuild = 0; BOOL wowRet, isWoW = FALSE; #if defined(_MSC_VER) # pragma warning(push) /* GetVersion was declared deprecated */ # pragma warning(disable : 4996) #endif dwVersion = GetVersion(); #if defined(_MSC_VER) # pragma warning(pop) #endif dwMajorVersion = (DWORD)(LOBYTE(LOWORD(dwVersion))); dwMinorVersion = (DWORD)(HIBYTE(LOWORD(dwVersion))); dwBuild = ((dwVersion < 0x80000000) ? (DWORD)(HIWORD(dwVersion)) : 0); (void)dwBuild; wowRet = IsWow64Process(GetCurrentProcess(), &isWoW); snprintf(name, sizeof(name), "Windows %u.%u%s", (unsigned)dwMajorVersion, (unsigned)dwMinorVersion, (wowRet ? (isWoW ? " (WoW64)" : "") : " (?)")); *sysName = mg_strdup(name); #elif defined(__ZEPHYR__) *sysName = mg_strdup("Zephyr OS"); #else struct utsname name; memset(&name, 0, sizeof(name)); uname(&name); *sysName = mg_strdup(name.sysname); #endif } static void legacy_init(const char **options) { const char *ports_option = config_options[LISTENING_PORTS].default_value; if (options) { const char **run_options = options; const char *optname = config_options[LISTENING_PORTS].name; /* Try to find the "listening_ports" option */ while (*run_options) { if (!strcmp(*run_options, optname)) { ports_option = run_options[1]; } run_options += 2; } } if (is_ssl_port_used(ports_option)) { /* Initialize with SSL support */ mg_init_library(MG_FEATURES_TLS); } else { /* Initialize without SSL support */ mg_init_library(MG_FEATURES_DEFAULT); } } CIVETWEB_API struct mg_context * mg_start2(struct mg_init_data *init, struct mg_error_data *error) { struct mg_context *ctx; const char *name, *value, *default_value; int idx, ok, workerthreadcount; unsigned int i; int itmp; void (*exit_callback)(const struct mg_context *ctx) = 0; const char **options = ((init != NULL) ? (init->configuration_options) : (NULL)); struct mg_workerTLS tls; if (error != NULL) { error->code = MG_ERROR_DATA_CODE_OK; error->code_sub = 0; if (error->text_buffer_size > 0) { *error->text = 0; } } if (mg_init_library_called == 0) { /* Legacy INIT, if mg_start is called without mg_init_library. * Note: This will cause a memory leak when unloading the library. */ legacy_init(options); } if (mg_init_library_called == 0) { if (error != NULL) { error->code = MG_ERROR_DATA_CODE_INIT_LIBRARY_FAILED; mg_snprintf(NULL, NULL, /* No truncation check for error buffers */ error->text, error->text_buffer_size, "%s", "Library uninitialized"); } return NULL; } /* Allocate context and initialize reasonable general case defaults. */ ctx = (struct mg_context *)mg_calloc(1, sizeof(*ctx)); if (ctx == NULL) { if (error != NULL) { error->code = MG_ERROR_DATA_CODE_OUT_OF_MEMORY; error->code_sub = (unsigned)sizeof(*ctx); mg_snprintf(NULL, NULL, /* No truncation check for error buffers */ error->text, error->text_buffer_size, "%s", "Out of memory"); } return NULL; } /* Random number generator will initialize at the first call */ ctx->dd.auth_nonce_mask = (uint64_t)get_random() ^ (uint64_t)(ptrdiff_t)(options); /* Save started thread index to reuse in other external API calls * For the sake of thread synchronization all non-civetweb threads * can be considered as single external thread */ ctx->starter_thread_idx = (unsigned)mg_atomic_inc(&thread_idx_max); tls.is_master = -1; /* Thread calling mg_start */ tls.thread_idx = ctx->starter_thread_idx; #if defined(_WIN32) tls.pthread_cond_helper_mutex = NULL; #endif pthread_setspecific(sTlsKey, &tls); ok = (0 == pthread_mutex_init(&ctx->thread_mutex, &pthread_mutex_attr)); #if !defined(ALTERNATIVE_QUEUE) ok &= (0 == pthread_cond_init(&ctx->sq_empty, NULL)); ok &= (0 == pthread_cond_init(&ctx->sq_full, NULL)); ctx->sq_blocked = 0; #endif ok &= (0 == pthread_mutex_init(&ctx->nonce_mutex, &pthread_mutex_attr)); #if defined(USE_LUA) ok &= (0 == pthread_mutex_init(&ctx->lua_bg_mutex, &pthread_mutex_attr)); #endif if (!ok) { unsigned error_id = (unsigned)ERRNO; const char *err_msg = "Cannot initialize thread synchronization objects"; /* Fatal error - abort start. However, this situation should never * occur in practice. */ mg_cry_ctx_internal(ctx, "%s", err_msg); if (error != NULL) { error->code = MG_ERROR_DATA_CODE_OS_ERROR; error->code_sub = error_id; mg_snprintf(NULL, NULL, /* No truncation check for error buffers */ error->text, error->text_buffer_size, "%s", err_msg); } mg_free(ctx); pthread_setspecific(sTlsKey, NULL); return NULL; } if ((init != NULL) && (init->callbacks != NULL)) { /* Set all callbacks except exit_context. */ ctx->callbacks = *init->callbacks; exit_callback = init->callbacks->exit_context; /* The exit callback is activated once the context is successfully * created. It should not be called, if an incomplete context object * is deleted during a failed initialization. */ ctx->callbacks.exit_context = 0; } ctx->user_data = ((init != NULL) ? (init->user_data) : (NULL)); ctx->dd.handlers = NULL; ctx->dd.next = NULL; #if defined(USE_LUA) lua_ctx_init(ctx); #endif /* Store options */ while (options && (name = *options++) != NULL) { idx = get_option_index(name); if (idx == -1) { mg_cry_ctx_internal(ctx, "Invalid option: %s", name); if (error != NULL) { error->code = MG_ERROR_DATA_CODE_INVALID_OPTION; error->code_sub = (unsigned)-1; mg_snprintf(NULL, NULL, /* No truncation check for error buffers */ error->text, error->text_buffer_size, "Invalid configuration option: %s", name); } free_context(ctx); pthread_setspecific(sTlsKey, NULL); return NULL; } else if ((value = *options++) == NULL) { mg_cry_ctx_internal(ctx, "%s: option value cannot be NULL", name); if (error != NULL) { error->code = MG_ERROR_DATA_CODE_INVALID_OPTION; error->code_sub = (unsigned)idx; mg_snprintf(NULL, NULL, /* No truncation check for error buffers */ error->text, error->text_buffer_size, "Invalid configuration option value: %s", name); } free_context(ctx); pthread_setspecific(sTlsKey, NULL); return NULL; } if (ctx->dd.config[idx] != NULL) { /* A duplicate configuration option is not an error - the last * option value will be used. */ mg_cry_ctx_internal(ctx, "warning: %s: duplicate option", name); mg_free(ctx->dd.config[idx]); } ctx->dd.config[idx] = mg_strdup_ctx(value, ctx); DEBUG_TRACE("[%s] -> [%s]", name, value); } /* Set default value if needed */ for (i = 0; config_options[i].name != NULL; i++) { default_value = config_options[i].default_value; if ((ctx->dd.config[i] == NULL) && (default_value != NULL)) { ctx->dd.config[i] = mg_strdup_ctx(default_value, ctx); } } /* Request size option */ itmp = atoi(ctx->dd.config[MAX_REQUEST_SIZE]); if (itmp < 1024) { mg_cry_ctx_internal(ctx, "%s too small", config_options[MAX_REQUEST_SIZE].name); if (error != NULL) { error->code = MG_ERROR_DATA_CODE_INVALID_OPTION; error->code_sub = (unsigned)MAX_REQUEST_SIZE; mg_snprintf(NULL, NULL, /* No truncation check for error buffers */ error->text, error->text_buffer_size, "Invalid configuration option value: %s", config_options[MAX_REQUEST_SIZE].name); } free_context(ctx); pthread_setspecific(sTlsKey, NULL); return NULL; } ctx->max_request_size = (unsigned)itmp; /* Queue length */ #if !defined(ALTERNATIVE_QUEUE) itmp = atoi(ctx->dd.config[CONNECTION_QUEUE_SIZE]); if (itmp < 1) { mg_cry_ctx_internal(ctx, "%s too small", config_options[CONNECTION_QUEUE_SIZE].name); if (error != NULL) { error->code = MG_ERROR_DATA_CODE_INVALID_OPTION; error->code_sub = CONNECTION_QUEUE_SIZE; mg_snprintf(NULL, NULL, /* No truncation check for error buffers */ error->text, error->text_buffer_size, "Invalid configuration option value: %s", config_options[CONNECTION_QUEUE_SIZE].name); } free_context(ctx); pthread_setspecific(sTlsKey, NULL); return NULL; } ctx->squeue = (struct socket *)mg_calloc((unsigned int)itmp, sizeof(struct socket)); if (ctx->squeue == NULL) { mg_cry_ctx_internal(ctx, "Out of memory: Cannot allocate %s", config_options[CONNECTION_QUEUE_SIZE].name); if (error != NULL) { error->code = MG_ERROR_DATA_CODE_OUT_OF_MEMORY; error->code_sub = (unsigned)itmp * (unsigned)sizeof(struct socket); mg_snprintf(NULL, NULL, /* No truncation check for error buffers */ error->text, error->text_buffer_size, "Out of memory: Cannot allocate %s", config_options[CONNECTION_QUEUE_SIZE].name); } free_context(ctx); pthread_setspecific(sTlsKey, NULL); return NULL; } ctx->sq_size = itmp; #endif /* Worker thread count option */ workerthreadcount = atoi(ctx->dd.config[NUM_THREADS]); if ((workerthreadcount > MAX_WORKER_THREADS) || (workerthreadcount <= 0)) { if (workerthreadcount <= 0) { mg_cry_ctx_internal(ctx, "%s", "Invalid number of worker threads"); } else { mg_cry_ctx_internal(ctx, "%s", "Too many worker threads"); } if (error != NULL) { error->code = MG_ERROR_DATA_CODE_INVALID_OPTION; error->code_sub = NUM_THREADS; mg_snprintf(NULL, NULL, /* No truncation check for error buffers */ error->text, error->text_buffer_size, "Invalid configuration option value: %s", config_options[NUM_THREADS].name); } free_context(ctx); pthread_setspecific(sTlsKey, NULL); return NULL; } /* Document root */ #if defined(NO_FILES) if (ctx->dd.config[DOCUMENT_ROOT] != NULL) { mg_cry_ctx_internal(ctx, "%s", "Document root must not be set"); if (error != NULL) { error->code = MG_ERROR_DATA_CODE_INVALID_OPTION; error->code_sub = (unsigned)DOCUMENT_ROOT; mg_snprintf(NULL, NULL, /* No truncation check for error buffers */ error->text, error->text_buffer_size, "Invalid configuration option value: %s", config_options[DOCUMENT_ROOT].name); } free_context(ctx); pthread_setspecific(sTlsKey, NULL); return NULL; } #endif get_system_name(&ctx->systemName); #if defined(USE_LUA) /* If a Lua background script has been configured, start it. */ ctx->lua_bg_log_available = 0; if (ctx->dd.config[LUA_BACKGROUND_SCRIPT] != NULL) { char ebuf[256]; struct vec opt_vec; struct vec eq_vec; const char *sparams; memset(ebuf, 0, sizeof(ebuf)); pthread_mutex_lock(&ctx->lua_bg_mutex); /* Create a Lua state, load all standard libraries and the mg table */ lua_State *state = mg_lua_context_script_prepare( ctx->dd.config[LUA_BACKGROUND_SCRIPT], ctx, ebuf, sizeof(ebuf)); if (!state) { mg_cry_ctx_internal(ctx, "lua_background_script load error: %s", ebuf); if (error != NULL) { error->code = MG_ERROR_DATA_CODE_SCRIPT_ERROR; mg_snprintf(NULL, NULL, /* No truncation check for error buffers */ error->text, error->text_buffer_size, "Error in script %s: %s", config_options[LUA_BACKGROUND_SCRIPT].name, ebuf); } pthread_mutex_unlock(&ctx->lua_bg_mutex); free_context(ctx); pthread_setspecific(sTlsKey, NULL); return NULL; } /* Add a table with parameters into mg.params */ sparams = ctx->dd.config[LUA_BACKGROUND_SCRIPT_PARAMS]; if (sparams && sparams[0]) { lua_getglobal(state, "mg"); lua_pushstring(state, "params"); lua_newtable(state); while ((sparams = next_option(sparams, &opt_vec, &eq_vec)) != NULL) { reg_llstring( state, opt_vec.ptr, opt_vec.len, eq_vec.ptr, eq_vec.len); if (mg_strncasecmp(sparams, opt_vec.ptr, opt_vec.len) == 0) break; } lua_rawset(state, -3); lua_pop(state, 1); } /* Call script */ state = mg_lua_context_script_run(state, ctx->dd.config[LUA_BACKGROUND_SCRIPT], ctx, ebuf, sizeof(ebuf)); if (!state) { mg_cry_ctx_internal(ctx, "lua_background_script start error: %s", ebuf); if (error != NULL) { error->code = MG_ERROR_DATA_CODE_SCRIPT_ERROR; mg_snprintf(NULL, NULL, /* No truncation check for error buffers */ error->text, error->text_buffer_size, "Error in script %s: %s", config_options[DOCUMENT_ROOT].name, ebuf); } pthread_mutex_unlock(&ctx->lua_bg_mutex); free_context(ctx); pthread_setspecific(sTlsKey, NULL); return NULL; } /* state remains valid */ ctx->lua_background_state = (void *)state; pthread_mutex_unlock(&ctx->lua_bg_mutex); } else { ctx->lua_background_state = 0; } #endif /* Step by step initialization of ctx - depending on build options */ #if !defined(NO_FILESYSTEMS) if (!set_gpass_option(ctx, NULL)) { const char *err_msg = "Invalid global password file"; /* Fatal error - abort start. */ mg_cry_ctx_internal(ctx, "%s", err_msg); if (error != NULL) { error->code = MG_ERROR_DATA_CODE_INVALID_PASS_FILE; mg_snprintf(NULL, NULL, /* No truncation check for error buffers */ error->text, error->text_buffer_size, "%s", err_msg); } free_context(ctx); pthread_setspecific(sTlsKey, NULL); return NULL; } #endif #if defined(USE_MBEDTLS) if (!mg_sslctx_init(ctx, NULL)) { const char *err_msg = "Error initializing SSL context"; /* Fatal error - abort start. */ mg_cry_ctx_internal(ctx, "%s", err_msg); if (error != NULL) { error->code = MG_ERROR_DATA_CODE_INIT_TLS_FAILED; mg_snprintf(NULL, NULL, /* No truncation check for error buffers */ error->text, error->text_buffer_size, "%s", err_msg); } free_context(ctx); pthread_setspecific(sTlsKey, NULL); return NULL; } #elif !defined(NO_SSL) if (!init_ssl_ctx(ctx, NULL)) { const char *err_msg = "Error initializing SSL context"; /* Fatal error - abort start. */ mg_cry_ctx_internal(ctx, "%s", err_msg); if (error != NULL) { error->code = MG_ERROR_DATA_CODE_INIT_TLS_FAILED; mg_snprintf(NULL, NULL, /* No truncation check for error buffers */ error->text, error->text_buffer_size, "%s", err_msg); } free_context(ctx); pthread_setspecific(sTlsKey, NULL); return NULL; } #endif if (!set_ports_option(ctx)) { const char *err_msg = "Failed to setup server ports"; /* Fatal error - abort start. */ mg_cry_ctx_internal(ctx, "%s", err_msg); if (error != NULL) { error->code = MG_ERROR_DATA_CODE_INIT_PORTS_FAILED; mg_snprintf(NULL, NULL, /* No truncation check for error buffers */ error->text, error->text_buffer_size, "%s", err_msg); } free_context(ctx); pthread_setspecific(sTlsKey, NULL); return NULL; } #if !defined(_WIN32) && !defined(__ZEPHYR__) if (!set_uid_option(ctx)) { const char *err_msg = "Failed to run as configured user"; /* Fatal error - abort start. */ mg_cry_ctx_internal(ctx, "%s", err_msg); if (error != NULL) { error->code = MG_ERROR_DATA_CODE_INIT_USER_FAILED; mg_snprintf(NULL, NULL, /* No truncation check for error buffers */ error->text, error->text_buffer_size, "%s", err_msg); } free_context(ctx); pthread_setspecific(sTlsKey, NULL); return NULL; } #endif if (!set_acl_option(ctx)) { const char *err_msg = "Failed to setup access control list"; /* Fatal error - abort start. */ mg_cry_ctx_internal(ctx, "%s", err_msg); if (error != NULL) { error->code = MG_ERROR_DATA_CODE_INIT_ACL_FAILED; mg_snprintf(NULL, NULL, /* No truncation check for error buffers */ error->text, error->text_buffer_size, "%s", err_msg); } free_context(ctx); pthread_setspecific(sTlsKey, NULL); return NULL; } ctx->cfg_worker_threads = ((unsigned int)(workerthreadcount)); ctx->worker_threadids = (pthread_t *)mg_calloc_ctx(ctx->cfg_worker_threads, sizeof(pthread_t), ctx); if (ctx->worker_threadids == NULL) { const char *err_msg = "Not enough memory for worker thread ID array"; mg_cry_ctx_internal(ctx, "%s", err_msg); if (error != NULL) { error->code = MG_ERROR_DATA_CODE_OUT_OF_MEMORY; error->code_sub = (unsigned)ctx->cfg_worker_threads * (unsigned)sizeof(pthread_t); mg_snprintf(NULL, NULL, /* No truncation check for error buffers */ error->text, error->text_buffer_size, "%s", err_msg); } free_context(ctx); pthread_setspecific(sTlsKey, NULL); return NULL; } ctx->worker_connections = (struct mg_connection *)mg_calloc_ctx(ctx->cfg_worker_threads, sizeof(struct mg_connection), ctx); if (ctx->worker_connections == NULL) { const char *err_msg = "Not enough memory for worker thread connection array"; mg_cry_ctx_internal(ctx, "%s", err_msg); if (error != NULL) { error->code = MG_ERROR_DATA_CODE_OUT_OF_MEMORY; error->code_sub = (unsigned)ctx->cfg_worker_threads * (unsigned)sizeof(struct mg_connection); mg_snprintf(NULL, NULL, /* No truncation check for error buffers */ error->text, error->text_buffer_size, "%s", err_msg); } free_context(ctx); pthread_setspecific(sTlsKey, NULL); return NULL; } #if defined(ALTERNATIVE_QUEUE) ctx->client_wait_events = (void **)mg_calloc_ctx(ctx->cfg_worker_threads, sizeof(ctx->client_wait_events[0]), ctx); if (ctx->client_wait_events == NULL) { const char *err_msg = "Not enough memory for worker event array"; mg_cry_ctx_internal(ctx, "%s", err_msg); mg_free(ctx->worker_threadids); if (error != NULL) { error->code = MG_ERROR_DATA_CODE_OUT_OF_MEMORY; error->code_sub = (unsigned)ctx->cfg_worker_threads * (unsigned)sizeof(ctx->client_wait_events[0]); mg_snprintf(NULL, NULL, /* No truncation check for error buffers */ error->text, error->text_buffer_size, "%s", err_msg); } free_context(ctx); pthread_setspecific(sTlsKey, NULL); return NULL; } ctx->client_socks = (struct socket *)mg_calloc_ctx(ctx->cfg_worker_threads, sizeof(ctx->client_socks[0]), ctx); if (ctx->client_socks == NULL) { const char *err_msg = "Not enough memory for worker socket array"; mg_cry_ctx_internal(ctx, "%s", err_msg); mg_free(ctx->client_wait_events); mg_free(ctx->worker_threadids); if (error != NULL) { error->code = MG_ERROR_DATA_CODE_OUT_OF_MEMORY; error->code_sub = (unsigned)ctx->cfg_worker_threads * (unsigned)sizeof(ctx->client_socks[0]); mg_snprintf(NULL, NULL, /* No truncation check for error buffers */ error->text, error->text_buffer_size, "%s", err_msg); } free_context(ctx); pthread_setspecific(sTlsKey, NULL); return NULL; } for (i = 0; (unsigned)i < ctx->cfg_worker_threads; i++) { ctx->client_wait_events[i] = event_create(); if (ctx->client_wait_events[i] == 0) { const char *err_msg = "Error creating worker event %i"; mg_cry_ctx_internal(ctx, err_msg, i); while (i > 0) { i--; event_destroy(ctx->client_wait_events[i]); } mg_free(ctx->client_socks); mg_free(ctx->client_wait_events); mg_free(ctx->worker_threadids); if (error != NULL) { error->code = MG_ERROR_DATA_CODE_OS_ERROR; error->code_sub = (unsigned)ERRNO; mg_snprintf(NULL, NULL, /* No truncation check for error buffers */ error->text, error->text_buffer_size, err_msg, i); } free_context(ctx); pthread_setspecific(sTlsKey, NULL); return NULL; } } #endif #if defined(USE_TIMERS) if (timers_init(ctx) != 0) { const char *err_msg = "Error creating timers"; mg_cry_ctx_internal(ctx, "%s", err_msg); if (error != NULL) { error->code = MG_ERROR_DATA_CODE_OS_ERROR; error->code_sub = (unsigned)ERRNO; mg_snprintf(NULL, NULL, /* No truncation check for error buffers */ error->text, error->text_buffer_size, "%s", err_msg); } free_context(ctx); pthread_setspecific(sTlsKey, NULL); return NULL; } #endif /* Context has been created - init user libraries */ if (ctx->callbacks.init_context) { ctx->callbacks.init_context(ctx); } /* From now, the context is successfully created. * When it is destroyed, the exit callback should be called. */ ctx->callbacks.exit_context = exit_callback; ctx->context_type = CONTEXT_SERVER; /* server context */ /* Start worker threads */ for (i = 0; i < ctx->cfg_worker_threads; i++) { /* worker_thread sets up the other fields */ ctx->worker_connections[i].phys_ctx = ctx; if (mg_start_thread_with_id(worker_thread, &ctx->worker_connections[i], &ctx->worker_threadids[i]) != 0) { long error_no = (long)ERRNO; /* thread was not created */ if (i > 0) { /* If the second, third, ... thread cannot be created, set a * warning, but keep running. */ mg_cry_ctx_internal(ctx, "Cannot start worker thread %i: error %ld", i + 1, error_no); /* If the server initialization should stop here, all * threads that have already been created must be stopped * first, before any free_context(ctx) call. */ } else { /* If the first worker thread cannot be created, stop * initialization and free the entire server context. */ mg_cry_ctx_internal(ctx, "Cannot create threads: error %ld", error_no); if (error != NULL) { error->code = MG_ERROR_DATA_CODE_OS_ERROR; error->code_sub = (unsigned)error_no; mg_snprintf( NULL, NULL, /* No truncation check for error buffers */ error->text, error->text_buffer_size, "Cannot create first worker thread: error %ld", error_no); } free_context(ctx); pthread_setspecific(sTlsKey, NULL); return NULL; } break; } } /* Start master (listening) thread */ mg_start_thread_with_id(master_thread, ctx, &ctx->masterthreadid); pthread_setspecific(sTlsKey, NULL); return ctx; } CIVETWEB_API struct mg_context * mg_start(const struct mg_callbacks *callbacks, void *user_data, const char **options) { struct mg_init_data init = {0}; init.callbacks = callbacks; init.user_data = user_data; init.configuration_options = options; return mg_start2(&init, NULL); } /* Add an additional domain to an already running web server. */ CIVETWEB_API int mg_start_domain2(struct mg_context *ctx, const char **options, struct mg_error_data *error) { const char *name; const char *value; const char *default_value; struct mg_domain_context *new_dom; struct mg_domain_context *dom; int idx, i; if (error != NULL) { error->code = MG_ERROR_DATA_CODE_OK; error->code_sub = 0; if (error->text_buffer_size > 0) { *error->text = 0; } } if ((ctx == NULL) || (options == NULL)) { if (error != NULL) { error->code = MG_ERROR_DATA_CODE_INVALID_PARAM; mg_snprintf(NULL, NULL, /* No truncation check for error buffers */ error->text, error->text_buffer_size, "%s", "Invalid parameters"); } return -1; } if (!STOP_FLAG_IS_ZERO(&ctx->stop_flag)) { if (error != NULL) { error->code = MG_ERROR_DATA_CODE_SERVER_STOPPED; mg_snprintf(NULL, NULL, /* No truncation check for error buffers */ error->text, error->text_buffer_size, "%s", "Server already stopped"); } return -7; } new_dom = (struct mg_domain_context *) mg_calloc_ctx(1, sizeof(struct mg_domain_context), ctx); if (!new_dom) { /* Out of memory */ if (error != NULL) { error->code = MG_ERROR_DATA_CODE_OUT_OF_MEMORY; error->code_sub = (unsigned)sizeof(struct mg_domain_context); mg_snprintf(NULL, NULL, /* No truncation check for error buffers */ error->text, error->text_buffer_size, "%s", "Out or memory"); } return -6; } /* Store options - TODO: unite duplicate code */ while (options && (name = *options++) != NULL) { idx = get_option_index(name); if (idx == -1) { mg_cry_ctx_internal(ctx, "Invalid option: %s", name); if (error != NULL) { error->code = MG_ERROR_DATA_CODE_INVALID_OPTION; error->code_sub = (unsigned)-1; mg_snprintf(NULL, NULL, /* No truncation check for error buffers */ error->text, error->text_buffer_size, "Invalid option: %s", name); } mg_free(new_dom); return -2; } else if ((value = *options++) == NULL) { mg_cry_ctx_internal(ctx, "%s: option value cannot be NULL", name); if (error != NULL) { error->code = MG_ERROR_DATA_CODE_INVALID_OPTION; error->code_sub = (unsigned)idx; mg_snprintf(NULL, NULL, /* No truncation check for error buffers */ error->text, error->text_buffer_size, "Invalid option value: %s", name); } mg_free(new_dom); return -2; } if (new_dom->config[idx] != NULL) { /* Duplicate option: Later values overwrite earlier ones. */ mg_cry_ctx_internal(ctx, "warning: %s: duplicate option", name); mg_free(new_dom->config[idx]); } new_dom->config[idx] = mg_strdup_ctx(value, ctx); DEBUG_TRACE("[%s] -> [%s]", name, value); } /* Authentication domain is mandatory */ /* TODO: Maybe use a new option hostname? */ if (!new_dom->config[AUTHENTICATION_DOMAIN]) { mg_cry_ctx_internal(ctx, "%s", "authentication domain required"); if (error != NULL) { error->code = MG_ERROR_DATA_CODE_MISSING_OPTION; error->code_sub = AUTHENTICATION_DOMAIN; mg_snprintf(NULL, NULL, /* No truncation check for error buffers */ error->text, error->text_buffer_size, "Mandatory option %s missing", config_options[AUTHENTICATION_DOMAIN].name); } mg_free(new_dom); return -4; } /* Set default value if needed. Take the config value from * ctx as a default value. */ for (i = 0; config_options[i].name != NULL; i++) { default_value = ctx->dd.config[i]; if ((new_dom->config[i] == NULL) && (default_value != NULL)) { new_dom->config[i] = mg_strdup_ctx(default_value, ctx); } } new_dom->handlers = NULL; new_dom->next = NULL; new_dom->nonce_count = 0; new_dom->auth_nonce_mask = get_random() ^ (get_random() << 31); #if defined(USE_LUA) && defined(USE_WEBSOCKET) new_dom->shared_lua_websockets = NULL; #endif #if !defined(NO_SSL) && !defined(USE_MBEDTLS) if (!init_ssl_ctx(ctx, new_dom)) { /* Init SSL failed */ if (error != NULL) { error->code = MG_ERROR_DATA_CODE_INIT_TLS_FAILED; mg_snprintf(NULL, NULL, /* No truncation check for error buffers */ error->text, error->text_buffer_size, "%s", "Initializing SSL context failed"); } mg_free(new_dom); return -3; } #endif /* Add element to linked list. */ mg_lock_context(ctx); idx = 0; dom = &(ctx->dd); for (;;) { if (!mg_strcasecmp(new_dom->config[AUTHENTICATION_DOMAIN], dom->config[AUTHENTICATION_DOMAIN])) { /* Domain collision */ mg_cry_ctx_internal(ctx, "domain %s already in use", new_dom->config[AUTHENTICATION_DOMAIN]); if (error != NULL) { error->code = MG_ERROR_DATA_CODE_DUPLICATE_DOMAIN; mg_snprintf(NULL, NULL, /* No truncation check for error buffers */ error->text, error->text_buffer_size, "Domain %s specified by %s is already in use", new_dom->config[AUTHENTICATION_DOMAIN], config_options[AUTHENTICATION_DOMAIN].name); } mg_free(new_dom); mg_unlock_context(ctx); return -5; } /* Count number of domains */ idx++; if (dom->next == NULL) { dom->next = new_dom; break; } dom = dom->next; } mg_unlock_context(ctx); /* Return domain number */ return idx; } CIVETWEB_API int mg_start_domain(struct mg_context *ctx, const char **options) { return mg_start_domain2(ctx, options, NULL); } /* Feature check API function */ CIVETWEB_API unsigned mg_check_feature(unsigned feature) { static const unsigned feature_set = 0 /* Set bits for available features according to API documentation. * This bit mask is created at compile time, according to the active * preprocessor defines. It is a single const value at runtime. */ #if !defined(NO_FILES) | MG_FEATURES_FILES #endif #if !defined(NO_SSL) || defined(USE_MBEDTLS) | MG_FEATURES_SSL #endif #if !defined(NO_CGI) | MG_FEATURES_CGI #endif #if defined(USE_IPV6) | MG_FEATURES_IPV6 #endif #if defined(USE_WEBSOCKET) | MG_FEATURES_WEBSOCKET #endif #if defined(USE_LUA) | MG_FEATURES_LUA #endif #if defined(USE_DUKTAPE) | MG_FEATURES_SSJS #endif #if !defined(NO_CACHING) | MG_FEATURES_CACHE #endif #if defined(USE_SERVER_STATS) | MG_FEATURES_STATS #endif #if defined(USE_ZLIB) | MG_FEATURES_COMPRESSION #endif #if defined(USE_HTTP2) | MG_FEATURES_HTTP2 #endif #if defined(USE_X_DOM_SOCKET) | MG_FEATURES_X_DOMAIN_SOCKET #endif /* Set some extra bits not defined in the API documentation. * These bits may change without further notice. */ #if defined(MG_LEGACY_INTERFACE) | 0x80000000u #endif #if defined(MG_EXPERIMENTAL_INTERFACES) | 0x40000000u #endif #if !defined(NO_RESPONSE_BUFFERING) | 0x20000000u #endif #if defined(MEMORY_DEBUGGING) | 0x10000000u #endif ; return (feature & feature_set); } static size_t mg_str_append(char **dst, char *end, const char *src) { size_t len = strlen(src); if (*dst != end) { /* Append src if enough space, or close dst. */ if ((size_t)(end - *dst) > len) { strcpy(*dst, src); *dst += len; } else { *dst = end; } } return len; } /* Get system information. It can be printed or stored by the caller. * Return the size of available information. */ CIVETWEB_API int mg_get_system_info(char *buffer, int buflen) { char *end, *append_eoobj = NULL, block[256]; size_t system_info_length = 0; #if defined(_WIN32) static const char eol[] = "\r\n", eoobj[] = "\r\n}\r\n"; #else static const char eol[] = "\n", eoobj[] = "\n}\n"; #endif if ((buffer == NULL) || (buflen < 1)) { buflen = 0; end = buffer; } else { *buffer = 0; end = buffer + buflen; } if (buflen > (int)(sizeof(eoobj) - 1)) { /* has enough space to append eoobj */ append_eoobj = buffer; if (end) { end -= sizeof(eoobj) - 1; } } system_info_length += mg_str_append(&buffer, end, "{"); /* Server version */ { const char *version = mg_version(); mg_snprintf(NULL, NULL, block, sizeof(block), "%s\"version\" : \"%s\"", eol, version); system_info_length += mg_str_append(&buffer, end, block); } /* System info */ { #if defined(_WIN32) DWORD dwVersion = 0; DWORD dwMajorVersion = 0; DWORD dwMinorVersion = 0; SYSTEM_INFO si; GetSystemInfo(&si); #if defined(_MSC_VER) # pragma warning(push) /* GetVersion was declared deprecated */ # pragma warning(disable : 4996) #endif dwVersion = GetVersion(); #if defined(_MSC_VER) # pragma warning(pop) #endif dwMajorVersion = (DWORD)(LOBYTE(LOWORD(dwVersion))); dwMinorVersion = (DWORD)(HIBYTE(LOWORD(dwVersion))); mg_snprintf(NULL, NULL, block, sizeof(block), ",%s\"os\" : \"Windows %u.%u\"", eol, (unsigned)dwMajorVersion, (unsigned)dwMinorVersion); system_info_length += mg_str_append(&buffer, end, block); mg_snprintf(NULL, NULL, block, sizeof(block), ",%s\"cpu\" : \"type %u, cores %u, mask %x\"", eol, (unsigned)si.wProcessorArchitecture, (unsigned)si.dwNumberOfProcessors, (unsigned)si.dwActiveProcessorMask); system_info_length += mg_str_append(&buffer, end, block); #elif defined(__ZEPHYR__) mg_snprintf(NULL, NULL, block, sizeof(block), ",%s\"os\" : \"%s %s\"", eol, "Zephyr OS", ZEPHYR_VERSION); system_info_length += mg_str_append(&buffer, end, block); #else struct utsname name; memset(&name, 0, sizeof(name)); uname(&name); mg_snprintf(NULL, NULL, block, sizeof(block), ",%s\"os\" : \"%s %s (%s) - %s\"", eol, name.sysname, name.version, name.release, name.machine); system_info_length += mg_str_append(&buffer, end, block); #endif } /* Features */ { mg_snprintf(NULL, NULL, block, sizeof(block), ",%s\"features\" : %lu" ",%s\"feature_list\" : \"Server:%s%s%s%s%s%s%s%s%s\"", eol, (unsigned long)mg_check_feature(0xFFFFFFFFu), eol, mg_check_feature(MG_FEATURES_FILES) ? " Files" : "", mg_check_feature(MG_FEATURES_SSL) ? " HTTPS" : "", mg_check_feature(MG_FEATURES_CGI) ? " CGI" : "", mg_check_feature(MG_FEATURES_IPV6) ? " IPv6" : "", mg_check_feature(MG_FEATURES_WEBSOCKET) ? " WebSockets" : "", mg_check_feature(MG_FEATURES_LUA) ? " Lua" : "", mg_check_feature(MG_FEATURES_SSJS) ? " JavaScript" : "", mg_check_feature(MG_FEATURES_CACHE) ? " Cache" : "", mg_check_feature(MG_FEATURES_STATS) ? " Stats" : ""); system_info_length += mg_str_append(&buffer, end, block); #if defined(USE_LUA) mg_snprintf(NULL, NULL, block, sizeof(block), ",%s\"lua_version\" : \"%u (%s)\"", eol, (unsigned)LUA_VERSION_NUM, LUA_RELEASE); system_info_length += mg_str_append(&buffer, end, block); #endif #if defined(USE_DUKTAPE) mg_snprintf(NULL, NULL, block, sizeof(block), ",%s\"javascript\" : \"Duktape %u.%u.%u\"", eol, (unsigned)DUK_VERSION / 10000, ((unsigned)DUK_VERSION / 100) % 100, (unsigned)DUK_VERSION % 100); system_info_length += mg_str_append(&buffer, end, block); #endif } /* Build identifier. If BUILD_DATE is not set, __DATE__ will be used. */ { #if defined(BUILD_DATE) const char *bd = BUILD_DATE; #else #if defined(GCC_DIAGNOSTIC) #if GCC_VERSION >= 40900 # pragma GCC diagnostic push /* Disable idiotic compiler warning -Wdate-time, appeared in gcc5. This * does not work in some versions. If "BUILD_DATE" is defined to some * string, it is used instead of __DATE__. */ # pragma GCC diagnostic ignored "-Wdate-time" #endif #endif const char *bd = __DATE__; #if defined(GCC_DIAGNOSTIC) #if GCC_VERSION >= 40900 # pragma GCC diagnostic pop #endif #endif #endif mg_snprintf( NULL, NULL, block, sizeof(block), ",%s\"build\" : \"%s\"", eol, bd); system_info_length += mg_str_append(&buffer, end, block); } /* Compiler information */ /* http://sourceforge.net/p/predef/wiki/Compilers/ */ { #if defined(_MSC_VER) mg_snprintf(NULL, NULL, block, sizeof(block), ",%s\"compiler\" : \"MSC: %u (%u)\"", eol, (unsigned)_MSC_VER, (unsigned)_MSC_FULL_VER); system_info_length += mg_str_append(&buffer, end, block); #elif defined(__MINGW64__) mg_snprintf(NULL, NULL, block, sizeof(block), ",%s\"compiler\" : \"MinGW64: %u.%u\"", eol, (unsigned)__MINGW64_VERSION_MAJOR, (unsigned)__MINGW64_VERSION_MINOR); system_info_length += mg_str_append(&buffer, end, block); mg_snprintf(NULL, NULL, block, sizeof(block), ",%s\"compiler\" : \"MinGW32: %u.%u\"", eol, (unsigned)__MINGW32_MAJOR_VERSION, (unsigned)__MINGW32_MINOR_VERSION); system_info_length += mg_str_append(&buffer, end, block); #elif defined(__MINGW32__) mg_snprintf(NULL, NULL, block, sizeof(block), ",%s\"compiler\" : \"MinGW32: %u.%u\"", eol, (unsigned)__MINGW32_MAJOR_VERSION, (unsigned)__MINGW32_MINOR_VERSION); system_info_length += mg_str_append(&buffer, end, block); #elif defined(__clang__) mg_snprintf(NULL, NULL, block, sizeof(block), ",%s\"compiler\" : \"clang: %u.%u.%u (%s)\"", eol, __clang_major__, __clang_minor__, __clang_patchlevel__, __clang_version__); system_info_length += mg_str_append(&buffer, end, block); #elif defined(__GNUC__) mg_snprintf(NULL, NULL, block, sizeof(block), ",%s\"compiler\" : \"gcc: %u.%u.%u\"", eol, (unsigned)__GNUC__, (unsigned)__GNUC_MINOR__, (unsigned)__GNUC_PATCHLEVEL__); system_info_length += mg_str_append(&buffer, end, block); #elif defined(__INTEL_COMPILER) mg_snprintf(NULL, NULL, block, sizeof(block), ",%s\"compiler\" : \"Intel C/C++: %u\"", eol, (unsigned)__INTEL_COMPILER); system_info_length += mg_str_append(&buffer, end, block); #elif defined(__BORLANDC__) mg_snprintf(NULL, NULL, block, sizeof(block), ",%s\"compiler\" : \"Borland C: 0x%x\"", eol, (unsigned)__BORLANDC__); system_info_length += mg_str_append(&buffer, end, block); #elif defined(__SUNPRO_C) mg_snprintf(NULL, NULL, block, sizeof(block), ",%s\"compiler\" : \"Solaris: 0x%x\"", eol, (unsigned)__SUNPRO_C); system_info_length += mg_str_append(&buffer, end, block); #else mg_snprintf(NULL, NULL, block, sizeof(block), ",%s\"compiler\" : \"other\"", eol); system_info_length += mg_str_append(&buffer, end, block); #endif } /* Determine 32/64 bit data mode. * see https://en.wikipedia.org/wiki/64-bit_computing */ { mg_snprintf(NULL, NULL, block, sizeof(block), ",%s\"data_model\" : \"int:%u/%u/%u/%u, float:%u/%u/%u, " "char:%u/%u, " "ptr:%u, size:%u, time:%u\"", eol, (unsigned)sizeof(short), (unsigned)sizeof(int), (unsigned)sizeof(long), (unsigned)sizeof(long long), (unsigned)sizeof(float), (unsigned)sizeof(double), (unsigned)sizeof(long double), (unsigned)sizeof(char), (unsigned)sizeof(wchar_t), (unsigned)sizeof(void *), (unsigned)sizeof(size_t), (unsigned)sizeof(time_t)); system_info_length += mg_str_append(&buffer, end, block); } /* Terminate string */ if (append_eoobj) { strcat(append_eoobj, eoobj); } system_info_length += sizeof(eoobj) - 1; return (int)system_info_length; } /* Get context information. It can be printed or stored by the caller. * Return the size of available information. */ CIVETWEB_API int mg_get_context_info(const struct mg_context *ctx, char *buffer, int buflen) { #if defined(USE_SERVER_STATS) char *end, *append_eoobj = NULL, block[256]; size_t context_info_length = 0; #if defined(_WIN32) static const char eol[] = "\r\n", eoobj[] = "\r\n}\r\n"; #else static const char eol[] = "\n", eoobj[] = "\n}\n"; #endif struct mg_memory_stat *ms = get_memory_stat((struct mg_context *)ctx); if ((buffer == NULL) || (buflen < 1)) { buflen = 0; end = buffer; } else { *buffer = 0; end = buffer + buflen; } if (buflen > (int)(sizeof(eoobj) - 1)) { /* has enough space to append eoobj */ append_eoobj = buffer; end -= sizeof(eoobj) - 1; } context_info_length += mg_str_append(&buffer, end, "{"); if (ms) { /* <-- should be always true */ /* Memory information */ int blockCount = (int)ms->blockCount; int64_t totalMemUsed = ms->totalMemUsed; int64_t maxMemUsed = ms->maxMemUsed; if (totalMemUsed > maxMemUsed) { maxMemUsed = totalMemUsed; } mg_snprintf(NULL, NULL, block, sizeof(block), "%s\"memory\" : {%s" "\"blocks\" : %i,%s" "\"used\" : %" INT64_FMT ",%s" "\"maxUsed\" : %" INT64_FMT "%s" "}", eol, eol, blockCount, eol, totalMemUsed, eol, maxMemUsed, eol); context_info_length += mg_str_append(&buffer, end, block); } if (ctx) { /* Declare all variables at begin of the block, to comply * with old C standards. */ char start_time_str[64] = {0}; char now_str[64] = {0}; time_t start_time = ctx->start_time; time_t now = time(NULL); int64_t total_data_read, total_data_written; int active_connections = (int)ctx->active_connections; int max_active_connections = (int)ctx->max_active_connections; int total_connections = (int)ctx->total_connections; if (active_connections > max_active_connections) { max_active_connections = active_connections; } if (active_connections > total_connections) { total_connections = active_connections; } /* Connections information */ mg_snprintf(NULL, NULL, block, sizeof(block), ",%s\"connections\" : {%s" "\"active\" : %i,%s" "\"maxActive\" : %i,%s" "\"total\" : %i%s" "}", eol, eol, active_connections, eol, max_active_connections, eol, total_connections, eol); context_info_length += mg_str_append(&buffer, end, block); /* Queue information */ #if !defined(ALTERNATIVE_QUEUE) mg_snprintf(NULL, NULL, block, sizeof(block), ",%s\"queue\" : {%s" "\"length\" : %i,%s" "\"filled\" : %i,%s" "\"maxFilled\" : %i,%s" "\"full\" : %s%s" "}", eol, eol, ctx->sq_size, eol, ctx->sq_head - ctx->sq_tail, eol, ctx->sq_max_fill, eol, (ctx->sq_blocked ? "true" : "false"), eol); context_info_length += mg_str_append(&buffer, end, block); #endif /* Requests information */ mg_snprintf(NULL, NULL, block, sizeof(block), ",%s\"requests\" : {%s" "\"total\" : %lu%s" "}", eol, eol, (unsigned long)ctx->total_requests, eol); context_info_length += mg_str_append(&buffer, end, block); /* Data information */ total_data_read = mg_atomic_add64((volatile int64_t *)&ctx->total_data_read, 0); total_data_written = mg_atomic_add64((volatile int64_t *)&ctx->total_data_written, 0); mg_snprintf(NULL, NULL, block, sizeof(block), ",%s\"data\" : {%s" "\"read\" : %" INT64_FMT ",%s" "\"written\" : %" INT64_FMT "%s" "}", eol, eol, total_data_read, eol, total_data_written, eol); context_info_length += mg_str_append(&buffer, end, block); /* Execution time information */ gmt_time_string(start_time_str, sizeof(start_time_str) - 1, &start_time); gmt_time_string(now_str, sizeof(now_str) - 1, &now); mg_snprintf(NULL, NULL, block, sizeof(block), ",%s\"time\" : {%s" "\"uptime\" : %.0f,%s" "\"start\" : \"%s\",%s" "\"now\" : \"%s\"%s" "}", eol, eol, difftime(now, start_time), eol, start_time_str, eol, now_str, eol); context_info_length += mg_str_append(&buffer, end, block); } /* Terminate string */ if (append_eoobj) { strcat(append_eoobj, eoobj); } context_info_length += sizeof(eoobj) - 1; return (int)context_info_length; #else (void)ctx; if ((buffer != NULL) && (buflen > 0)) { *buffer = 0; } return 0; #endif } CIVETWEB_API void mg_disable_connection_keep_alive(struct mg_connection *conn) { /* https://github.com/civetweb/civetweb/issues/727 */ if (conn != NULL) { conn->must_close = 1; } } #if defined(MG_EXPERIMENTAL_INTERFACES) /* Get connection information. It can be printed or stored by the caller. * Return the size of available information. */ CIVETWEB_API int mg_get_connection_info(const struct mg_context *ctx, int idx, char *buffer, int buflen) { const struct mg_connection *conn; const struct mg_request_info *ri; char *end, *append_eoobj = NULL, block[256]; size_t connection_info_length = 0; int state = 0; const char *state_str = "unknown"; #if defined(_WIN32) static const char eol[] = "\r\n", eoobj[] = "\r\n}\r\n"; #else static const char eol[] = "\n", eoobj[] = "\n}\n"; #endif if ((buffer == NULL) || (buflen < 1)) { buflen = 0; end = buffer; } else { *buffer = 0; end = buffer + buflen; } if (buflen > (int)(sizeof(eoobj) - 1)) { /* has enough space to append eoobj */ append_eoobj = buffer; end -= sizeof(eoobj) - 1; } if ((ctx == NULL) || (idx < 0)) { /* Parameter error */ return 0; } if ((unsigned)idx >= ctx->cfg_worker_threads) { /* Out of range */ return 0; } /* Take connection [idx]. This connection is not locked in * any way, so some other thread might use it. */ conn = (ctx->worker_connections) + idx; /* Initialize output string */ connection_info_length += mg_str_append(&buffer, end, "{"); /* Init variables */ ri = &(conn->request_info); #if defined(USE_SERVER_STATS) state = conn->conn_state; /* State as string */ switch (state) { case 0: state_str = "undefined"; break; case 1: state_str = "not used"; break; case 2: state_str = "init"; break; case 3: state_str = "ready"; break; case 4: state_str = "processing"; break; case 5: state_str = "processed"; break; case 6: state_str = "to close"; break; case 7: state_str = "closing"; break; case 8: state_str = "closed"; break; case 9: state_str = "done"; break; } #endif /* Connection info */ if ((state >= 3) && (state < 9)) { mg_snprintf(NULL, NULL, block, sizeof(block), "%s\"connection\" : {%s" "\"remote\" : {%s" "\"protocol\" : \"%s\",%s" "\"addr\" : \"%s\",%s" "\"port\" : %u%s" "},%s" "\"handled_requests\" : %u%s" "}", eol, eol, eol, get_proto_name(conn), eol, ri->remote_addr, eol, ri->remote_port, eol, eol, conn->handled_requests, eol); connection_info_length += mg_str_append(&buffer, end, block); } /* Request info */ if ((state >= 4) && (state < 6)) { mg_snprintf(NULL, NULL, block, sizeof(block), "%s%s\"request_info\" : {%s" "\"method\" : \"%s\",%s" "\"uri\" : \"%s\",%s" "\"query\" : %s%s%s%s" "}", (connection_info_length > 1 ? "," : ""), eol, eol, ri->request_method, eol, ri->request_uri, eol, ri->query_string ? "\"" : "", ri->query_string ? ri->query_string : "null", ri->query_string ? "\"" : "", eol); connection_info_length += mg_str_append(&buffer, end, block); } /* Execution time information */ if ((state >= 2) && (state < 9)) { char start_time_str[64] = {0}; char close_time_str[64] = {0}; time_t start_time = conn->conn_birth_time; time_t close_time = 0; double time_diff; gmt_time_string(start_time_str, sizeof(start_time_str) - 1, &start_time); #if defined(USE_SERVER_STATS) close_time = conn->conn_close_time; #endif if (close_time != 0) { time_diff = difftime(close_time, start_time); gmt_time_string(close_time_str, sizeof(close_time_str) - 1, &close_time); } else { time_t now = time(NULL); time_diff = difftime(now, start_time); close_time_str[0] = 0; /* or use "now" ? */ } mg_snprintf(NULL, NULL, block, sizeof(block), "%s%s\"time\" : {%s" "\"uptime\" : %.0f,%s" "\"start\" : \"%s\",%s" "\"closed\" : \"%s\"%s" "}", (connection_info_length > 1 ? "," : ""), eol, eol, time_diff, eol, start_time_str, eol, close_time_str, eol); connection_info_length += mg_str_append(&buffer, end, block); } /* Remote user name */ if ((ri->remote_user) && (state < 9)) { mg_snprintf(NULL, NULL, block, sizeof(block), "%s%s\"user\" : {%s" "\"name\" : \"%s\",%s" "}", (connection_info_length > 1 ? "," : ""), eol, eol, ri->remote_user, eol); connection_info_length += mg_str_append(&buffer, end, block); } /* Data block */ if (state >= 3) { mg_snprintf(NULL, NULL, block, sizeof(block), "%s%s\"data\" : {%s" "\"read\" : %" INT64_FMT ",%s" "\"written\" : %" INT64_FMT "%s" "}", (connection_info_length > 1 ? "," : ""), eol, eol, conn->consumed_content, eol, conn->num_bytes_sent, eol); connection_info_length += mg_str_append(&buffer, end, block); } /* State */ mg_snprintf(NULL, NULL, block, sizeof(block), "%s%s\"state\" : \"%s\"", (connection_info_length > 1 ? "," : ""), eol, state_str); connection_info_length += mg_str_append(&buffer, end, block); /* Terminate string */ if (append_eoobj) { strcat(append_eoobj, eoobj); } connection_info_length += sizeof(eoobj) - 1; return (int)connection_info_length; } #if 0 /* Get handler information. It can be printed or stored by the caller. * Return the size of available information. */ CIVETWEB_API int mg_get_handler_info(struct mg_context *ctx, char *buffer, int buflen) { int handler_info_len = 0; struct mg_handler_info *tmp_rh; mg_lock_context(ctx); for (tmp_rh = ctx->dd.handlers; tmp_rh != NULL; tmp_rh = tmp_rh->next) { if (buflen > handler_info_len+ tmp_rh->uri_len) { memcpy(buffer+handler_info_len, tmp_rh->uri, tmp_rh->uri_len); } handler_info_len += tmp_rh->uri_len; switch (tmp_rh->handler_type) { case REQUEST_HANDLER: (void)tmp_rh->handler; break; case WEBSOCKET_HANDLER: (void)tmp_rh->connect_handler; (void) tmp_rh->ready_handler; (void) tmp_rh->data_handler; (void) tmp_rh->close_handler; break; case AUTH_HANDLER: (void) tmp_rh->auth_handler; break; } (void)cbdata; } mg_unlock_context(ctx); return handler_info_len; } #endif #endif /* Initialize this library. This function does not need to be thread safe. */ CIVETWEB_API unsigned mg_init_library(unsigned features) { unsigned features_to_init = mg_check_feature(features & 0xFFu); unsigned features_inited = features_to_init; if (mg_init_library_called <= 0) { /* Not initialized yet */ if (0 != pthread_mutex_init(&global_lock_mutex, NULL)) { return 0; } } mg_global_lock(); if (mg_init_library_called <= 0) { int i; size_t len; #if defined(_WIN32) int file_mutex_init = 1; int wsa = 1; #else int mutexattr_init = 1; #endif int failed = 1; int key_create = pthread_key_create(&sTlsKey, tls_dtor); if (key_create == 0) { #if defined(_WIN32) file_mutex_init = pthread_mutex_init(&global_log_file_lock, &pthread_mutex_attr); if (file_mutex_init == 0) { /* Start WinSock */ WSADATA data; failed = wsa = WSAStartup(MAKEWORD(2, 2), &data); } #else mutexattr_init = pthread_mutexattr_init(&pthread_mutex_attr); if (mutexattr_init == 0) { failed = pthread_mutexattr_settype(&pthread_mutex_attr, PTHREAD_MUTEX_RECURSIVE); } #endif } if (failed) { #if defined(_WIN32) if (wsa == 0) { (void)WSACleanup(); } if (file_mutex_init == 0) { (void)pthread_mutex_destroy(&global_log_file_lock); } #else if (mutexattr_init == 0) { (void)pthread_mutexattr_destroy(&pthread_mutex_attr); } #endif if (key_create == 0) { (void)pthread_key_delete(sTlsKey); } mg_global_unlock(); (void)pthread_mutex_destroy(&global_lock_mutex); return 0; } len = 1; for (i = 0; http_methods[i].name != NULL; i++) { size_t sl = strlen(http_methods[i].name); len += sl; if (i > 0) { len += 2; } } all_methods = (char *)mg_malloc(len); if (!all_methods) { /* Must never happen */ mg_global_unlock(); (void)pthread_mutex_destroy(&global_lock_mutex); return 0; } all_methods[0] = 0; for (i = 0; http_methods[i].name != NULL; i++) { if (i > 0) { strcat(all_methods, ", "); strcat(all_methods, http_methods[i].name); } else { strcpy(all_methods, http_methods[i].name); } } } #if defined(USE_LUA) lua_init_optional_libraries(); #endif #if (defined(OPENSSL_API_1_0) || defined(OPENSSL_API_1_1) \ || defined(OPENSSL_API_3_0)) \ && !defined(NO_SSL) if (features_to_init & MG_FEATURES_SSL) { if (!mg_openssl_initialized) { char ebuf[128]; if (initialize_openssl(ebuf, sizeof(ebuf))) { mg_openssl_initialized = 1; } else { (void)ebuf; DEBUG_TRACE("Initializing SSL failed: %s", ebuf); features_inited &= ~((unsigned)(MG_FEATURES_SSL)); } } else { /* ssl already initialized */ } } #endif if (mg_init_library_called <= 0) { mg_init_library_called = 1; } else { mg_init_library_called++; } mg_global_unlock(); return features_inited; } /* Un-initialize this library. */ CIVETWEB_API unsigned mg_exit_library(void) { if (mg_init_library_called <= 0) { return 0; } mg_global_lock(); mg_init_library_called--; if (mg_init_library_called == 0) { #if (defined(OPENSSL_API_1_0) || defined(OPENSSL_API_1_1)) && !defined(NO_SSL) if (mg_openssl_initialized) { uninitialize_openssl(); mg_openssl_initialized = 0; } #endif #if defined(_WIN32) (void)WSACleanup(); (void)pthread_mutex_destroy(&global_log_file_lock); #else (void)pthread_mutexattr_destroy(&pthread_mutex_attr); #endif (void)pthread_key_delete(sTlsKey); #if defined(USE_LUA) lua_exit_optional_libraries(); #endif mg_free(all_methods); all_methods = NULL; mg_global_unlock(); (void)pthread_mutex_destroy(&global_lock_mutex); return 1; } mg_global_unlock(); return 1; } /* End of civetweb.c */ webfakes/src/cleancall.h0000644000176200001440000000254514323222672014730 0ustar liggesusers#ifndef CLEANCALL_H #define CLEANCALL_H #include #include // -------------------------------------------------------------------- // Internals // -------------------------------------------------------------------- typedef union {void* p; DL_FUNC fn;} fn_ptr; #if (defined(R_VERSION) && R_VERSION < R_Version(3, 4, 0)) SEXP R_MakeExternalPtrFn(DL_FUNC p, SEXP tag, SEXP prot); DL_FUNC R_ExternalPtrAddrFn(SEXP s); #endif // -------------------------------------------------------------------- // API for packages that embed cleancall // -------------------------------------------------------------------- // The R API does not have a setter for external function pointers SEXP cleancall_MakeExternalPtrFn(DL_FUNC p, SEXP tag, SEXP prot); void cleancall_SetExternalPtrAddrFn(SEXP s, DL_FUNC p); #define CLEANCALL_METHOD_RECORD \ {"cleancall_call", (DL_FUNC) &cleancall_call, 2} SEXP cleancall_call(SEXP args, SEXP env); extern SEXP cleancall_fns_dot_call; void cleancall_init(void); // -------------------------------------------------------------------- // Public API // -------------------------------------------------------------------- SEXP r_with_cleanup_context(SEXP (*fn)(void* data), void* data); void r_call_on_exit(void (*fn)(void* data), void* data); void r_call_on_early_exit(void (*fn)(void* data), void* data); #endif webfakes/src/md5.h0000644000176200001440000003634614740430263013505 0ustar liggesusers/* * This an amalgamation of md5.c and md5.h into a single file * with all static declaration to reduce linker conflicts * in Civetweb. * * The MD5_STATIC declaration was added to facilitate static * inclusion. * No Face Press, LLC */ /* $Id: md5.h,v 1.4 2002/04/13 19:20:28 lpd Exp $ */ /* Independent implementation of MD5 (RFC 1321). This code implements the MD5 Algorithm defined in RFC 1321, whose text is available at http://www.ietf.org/rfc/rfc1321.txt The code is derived from the text of the RFC, including the test suite (section A.5) but excluding the rest of Appendix A. It does not include any code or documentation that is identified in the RFC as being copyrighted. The original and principal author of md5.h is L. Peter Deutsch . Other authors are noted in the change history that follows (in reverse chronological order): 2002-04-13 lpd Removed support for non-ANSI compilers; removed references to Ghostscript; clarified derivation from RFC 1321; now handles byte order either statically or dynamically. 1999-11-04 lpd Edited comments slightly for automatic TOC extraction. 1999-10-18 lpd Fixed typo in header comment (ansi2knr rather than md5); added conditionalization for C++ compilation from Martin Purschke . 1999-05-03 lpd Original version. */ #if !defined(md5_INCLUDED) #define md5_INCLUDED /* * This package supports both compile-time and run-time determination of CPU * byte order. If ARCH_IS_BIG_ENDIAN is defined as 0, the code will be * compiled to run only on little-endian CPUs; if ARCH_IS_BIG_ENDIAN is * defined as non-zero, the code will be compiled to run only on big-endian * CPUs; if ARCH_IS_BIG_ENDIAN is not defined, the code will be compiled to * run on either big- or little-endian CPUs, but will run slightly less * efficiently on either one than if ARCH_IS_BIG_ENDIAN is defined. */ typedef unsigned char md5_byte_t; /* 8-bit byte */ typedef unsigned int md5_word_t; /* 32-bit word */ /* Define the state of the MD5 Algorithm. */ typedef struct md5_state_s { md5_word_t count[2]; /* message length in bits, lsw first */ md5_word_t abcd[4]; /* digest buffer */ md5_byte_t buf[64]; /* accumulate block */ } md5_state_t; #if defined(__cplusplus) extern "C" { #endif /* Initialize the algorithm. */ MD5_STATIC void md5_init(md5_state_t *pms); /* Append a string to the message. */ MD5_STATIC void md5_append(md5_state_t *pms, const md5_byte_t *data, size_t nbytes); /* Finish the message and return the digest. */ MD5_STATIC void md5_finish(md5_state_t *pms, md5_byte_t digest[16]); #if defined(__cplusplus) } /* end extern "C" */ #endif #endif /* md5_INCLUDED */ /* Copyright (C) 1999, 2000, 2002 Aladdin Enterprises. All rights reserved. This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. L. Peter Deutsch ghost@aladdin.com */ /* $Id: md5.c,v 1.6 2002/04/13 19:20:28 lpd Exp $ */ /* Independent implementation of MD5 (RFC 1321). This code implements the MD5 Algorithm defined in RFC 1321, whose text is available at http://www.ietf.org/rfc/rfc1321.txt The code is derived from the text of the RFC, including the test suite (section A.5) but excluding the rest of Appendix A. It does not include any code or documentation that is identified in the RFC as being copyrighted. The original and principal author of md5.c is L. Peter Deutsch . Other authors are noted in the change history that follows (in reverse chronological order): 2002-04-13 lpd Clarified derivation from RFC 1321; now handles byte order either statically or dynamically; added missing #include in library. 2002-03-11 lpd Corrected argument list for main(), and added int return type, in test program and T value program. 2002-02-21 lpd Added missing #include in test program. 2000-07-03 lpd Patched to eliminate warnings about "constant is unsigned in ANSI C, signed in traditional"; made test program self-checking. 1999-11-04 lpd Edited comments slightly for automatic TOC extraction. 1999-10-18 lpd Fixed typo in header comment (ansi2knr rather than md5). 1999-05-03 lpd Original version. */ #if !defined(MD5_STATIC) #include #include #endif #undef BYTE_ORDER /* 1 = big-endian, -1 = little-endian, 0 = unknown */ #if defined(ARCH_IS_BIG_ENDIAN) #define BYTE_ORDER (ARCH_IS_BIG_ENDIAN ? 1 : -1) #else #define BYTE_ORDER (0) #endif #define T_MASK ((md5_word_t)~0) #define T1 /* 0xd76aa478 */ (T_MASK ^ 0x28955b87) #define T2 /* 0xe8c7b756 */ (T_MASK ^ 0x173848a9) #define T3 (0x242070db) #define T4 /* 0xc1bdceee */ (T_MASK ^ 0x3e423111) #define T5 /* 0xf57c0faf */ (T_MASK ^ 0x0a83f050) #define T6 (0x4787c62a) #define T7 /* 0xa8304613 */ (T_MASK ^ 0x57cfb9ec) #define T8 /* 0xfd469501 */ (T_MASK ^ 0x02b96afe) #define T9 (0x698098d8) #define T10 /* 0x8b44f7af */ (T_MASK ^ 0x74bb0850) #define T11 /* 0xffff5bb1 */ (T_MASK ^ 0x0000a44e) #define T12 /* 0x895cd7be */ (T_MASK ^ 0x76a32841) #define T13 (0x6b901122) #define T14 /* 0xfd987193 */ (T_MASK ^ 0x02678e6c) #define T15 /* 0xa679438e */ (T_MASK ^ 0x5986bc71) #define T16 (0x49b40821) #define T17 /* 0xf61e2562 */ (T_MASK ^ 0x09e1da9d) #define T18 /* 0xc040b340 */ (T_MASK ^ 0x3fbf4cbf) #define T19 (0x265e5a51) #define T20 /* 0xe9b6c7aa */ (T_MASK ^ 0x16493855) #define T21 /* 0xd62f105d */ (T_MASK ^ 0x29d0efa2) #define T22 (0x02441453) #define T23 /* 0xd8a1e681 */ (T_MASK ^ 0x275e197e) #define T24 /* 0xe7d3fbc8 */ (T_MASK ^ 0x182c0437) #define T25 (0x21e1cde6) #define T26 /* 0xc33707d6 */ (T_MASK ^ 0x3cc8f829) #define T27 /* 0xf4d50d87 */ (T_MASK ^ 0x0b2af278) #define T28 (0x455a14ed) #define T29 /* 0xa9e3e905 */ (T_MASK ^ 0x561c16fa) #define T30 /* 0xfcefa3f8 */ (T_MASK ^ 0x03105c07) #define T31 (0x676f02d9) #define T32 /* 0x8d2a4c8a */ (T_MASK ^ 0x72d5b375) #define T33 /* 0xfffa3942 */ (T_MASK ^ 0x0005c6bd) #define T34 /* 0x8771f681 */ (T_MASK ^ 0x788e097e) #define T35 (0x6d9d6122) #define T36 /* 0xfde5380c */ (T_MASK ^ 0x021ac7f3) #define T37 /* 0xa4beea44 */ (T_MASK ^ 0x5b4115bb) #define T38 (0x4bdecfa9) #define T39 /* 0xf6bb4b60 */ (T_MASK ^ 0x0944b49f) #define T40 /* 0xbebfbc70 */ (T_MASK ^ 0x4140438f) #define T41 (0x289b7ec6) #define T42 /* 0xeaa127fa */ (T_MASK ^ 0x155ed805) #define T43 /* 0xd4ef3085 */ (T_MASK ^ 0x2b10cf7a) #define T44 (0x04881d05) #define T45 /* 0xd9d4d039 */ (T_MASK ^ 0x262b2fc6) #define T46 /* 0xe6db99e5 */ (T_MASK ^ 0x1924661a) #define T47 (0x1fa27cf8) #define T48 /* 0xc4ac5665 */ (T_MASK ^ 0x3b53a99a) #define T49 /* 0xf4292244 */ (T_MASK ^ 0x0bd6ddbb) #define T50 (0x432aff97) #define T51 /* 0xab9423a7 */ (T_MASK ^ 0x546bdc58) #define T52 /* 0xfc93a039 */ (T_MASK ^ 0x036c5fc6) #define T53 (0x655b59c3) #define T54 /* 0x8f0ccc92 */ (T_MASK ^ 0x70f3336d) #define T55 /* 0xffeff47d */ (T_MASK ^ 0x00100b82) #define T56 /* 0x85845dd1 */ (T_MASK ^ 0x7a7ba22e) #define T57 (0x6fa87e4f) #define T58 /* 0xfe2ce6e0 */ (T_MASK ^ 0x01d3191f) #define T59 /* 0xa3014314 */ (T_MASK ^ 0x5cfebceb) #define T60 (0x4e0811a1) #define T61 /* 0xf7537e82 */ (T_MASK ^ 0x08ac817d) #define T62 /* 0xbd3af235 */ (T_MASK ^ 0x42c50dca) #define T63 (0x2ad7d2bb) #define T64 /* 0xeb86d391 */ (T_MASK ^ 0x14792c6e) static void md5_process(md5_state_t *pms, const md5_byte_t *data /*[64]*/) { md5_word_t a = pms->abcd[0], b = pms->abcd[1], c = pms->abcd[2], d = pms->abcd[3]; md5_word_t t; #if BYTE_ORDER > 0 /* Define storage only for big-endian CPUs. */ md5_word_t X[16]; #else /* Define storage for little-endian or both types of CPUs. */ md5_word_t xbuf[16]; const md5_word_t *X; #endif { #if BYTE_ORDER == 0 /* * Determine dynamically whether this is a big-endian or * little-endian machine, since we can use a more efficient * algorithm on the latter. */ static const int w = 1; if (*((const md5_byte_t *)&w)) /* dynamic little-endian */ #endif #if BYTE_ORDER <= 0 /* little-endian */ { /* * On little-endian machines, we can process properly aligned * data without copying it. */ if (!(((uintptr_t)data) & 3)) { /* data are properly aligned, a direct assignment is possible */ /* cast through a (void *) should avoid a compiler warning, see https://github.com/bel2125/civetweb/issues/94#issuecomment-98112861 */ X = (const md5_word_t *)(const void *)data; } else { /* not aligned */ memcpy(xbuf, data, 64); X = xbuf; } } #endif #if BYTE_ORDER == 0 else /* dynamic big-endian */ #endif #if BYTE_ORDER >= 0 /* big-endian */ { /* * On big-endian machines, we must arrange the bytes in the * right order. */ const md5_byte_t *xp = data; int i; #if BYTE_ORDER == 0 X = xbuf; /* (dynamic only) */ #else #define xbuf X /* (static only) */ #endif for (i = 0; i < 16; ++i, xp += 4) xbuf[i] = (md5_word_t)(xp[0]) + (md5_word_t)(xp[1] << 8) + (md5_word_t)(xp[2] << 16) + (md5_word_t)(xp[3] << 24); } #endif } #define ROTATE_LEFT(x, n) (((x) << (n)) | ((x) >> (32 - (n)))) /* Round 1. */ /* Let [abcd k s i] denote the operation a = b + ((a + F(b,c,d) + X[k] + T[i]) <<< s). */ #define F(x, y, z) (((x) & (y)) | (~(x) & (z))) #define SET(a, b, c, d, k, s, Ti) \ t = (a) + F(b, c, d) + X[k] + (Ti); \ (a) = ROTATE_LEFT(t, s) + (b) /* Do the following 16 operations. */ SET(a, b, c, d, 0, 7, T1); SET(d, a, b, c, 1, 12, T2); SET(c, d, a, b, 2, 17, T3); SET(b, c, d, a, 3, 22, T4); SET(a, b, c, d, 4, 7, T5); SET(d, a, b, c, 5, 12, T6); SET(c, d, a, b, 6, 17, T7); SET(b, c, d, a, 7, 22, T8); SET(a, b, c, d, 8, 7, T9); SET(d, a, b, c, 9, 12, T10); SET(c, d, a, b, 10, 17, T11); SET(b, c, d, a, 11, 22, T12); SET(a, b, c, d, 12, 7, T13); SET(d, a, b, c, 13, 12, T14); SET(c, d, a, b, 14, 17, T15); SET(b, c, d, a, 15, 22, T16); #undef SET /* Round 2. */ /* Let [abcd k s i] denote the operation a = b + ((a + G(b,c,d) + X[k] + T[i]) <<< s). */ #define G(x, y, z) (((x) & (z)) | ((y) & ~(z))) #define SET(a, b, c, d, k, s, Ti) \ t = (a) + G(b, c, d) + X[k] + (Ti); \ (a) = ROTATE_LEFT(t, s) + (b) /* Do the following 16 operations. */ SET(a, b, c, d, 1, 5, T17); SET(d, a, b, c, 6, 9, T18); SET(c, d, a, b, 11, 14, T19); SET(b, c, d, a, 0, 20, T20); SET(a, b, c, d, 5, 5, T21); SET(d, a, b, c, 10, 9, T22); SET(c, d, a, b, 15, 14, T23); SET(b, c, d, a, 4, 20, T24); SET(a, b, c, d, 9, 5, T25); SET(d, a, b, c, 14, 9, T26); SET(c, d, a, b, 3, 14, T27); SET(b, c, d, a, 8, 20, T28); SET(a, b, c, d, 13, 5, T29); SET(d, a, b, c, 2, 9, T30); SET(c, d, a, b, 7, 14, T31); SET(b, c, d, a, 12, 20, T32); #undef SET /* Round 3. */ /* Let [abcd k s t] denote the operation a = b + ((a + H(b,c,d) + X[k] + T[i]) <<< s). */ #define H(x, y, z) ((x) ^ (y) ^ (z)) #define SET(a, b, c, d, k, s, Ti) \ t = (a) + H(b, c, d) + X[k] + (Ti); \ (a) = ROTATE_LEFT(t, s) + b /* Do the following 16 operations. */ SET(a, b, c, d, 5, 4, T33); SET(d, a, b, c, 8, 11, T34); SET(c, d, a, b, 11, 16, T35); SET(b, c, d, a, 14, 23, T36); SET(a, b, c, d, 1, 4, T37); SET(d, a, b, c, 4, 11, T38); SET(c, d, a, b, 7, 16, T39); SET(b, c, d, a, 10, 23, T40); SET(a, b, c, d, 13, 4, T41); SET(d, a, b, c, 0, 11, T42); SET(c, d, a, b, 3, 16, T43); SET(b, c, d, a, 6, 23, T44); SET(a, b, c, d, 9, 4, T45); SET(d, a, b, c, 12, 11, T46); SET(c, d, a, b, 15, 16, T47); SET(b, c, d, a, 2, 23, T48); #undef SET /* Round 4. */ /* Let [abcd k s t] denote the operation a = b + ((a + I(b,c,d) + X[k] + T[i]) <<< s). */ #define I(x, y, z) ((y) ^ ((x) | ~(z))) #define SET(a, b, c, d, k, s, Ti) \ t = (a) + I(b, c, d) + X[k] + (Ti); \ (a) = ROTATE_LEFT(t, s) + (b) /* Do the following 16 operations. */ SET(a, b, c, d, 0, 6, T49); SET(d, a, b, c, 7, 10, T50); SET(c, d, a, b, 14, 15, T51); SET(b, c, d, a, 5, 21, T52); SET(a, b, c, d, 12, 6, T53); SET(d, a, b, c, 3, 10, T54); SET(c, d, a, b, 10, 15, T55); SET(b, c, d, a, 1, 21, T56); SET(a, b, c, d, 8, 6, T57); SET(d, a, b, c, 15, 10, T58); SET(c, d, a, b, 6, 15, T59); SET(b, c, d, a, 13, 21, T60); SET(a, b, c, d, 4, 6, T61); SET(d, a, b, c, 11, 10, T62); SET(c, d, a, b, 2, 15, T63); SET(b, c, d, a, 9, 21, T64); #undef SET /* Then perform the following additions. (That is increment each of the four registers by the value it had before this block was started.) */ pms->abcd[0] += a; pms->abcd[1] += b; pms->abcd[2] += c; pms->abcd[3] += d; } MD5_STATIC void md5_init(md5_state_t *pms) { pms->count[0] = pms->count[1] = 0; pms->abcd[0] = 0x67452301; pms->abcd[1] = /*0xefcdab89*/ T_MASK ^ 0x10325476; pms->abcd[2] = /*0x98badcfe*/ T_MASK ^ 0x67452301; pms->abcd[3] = 0x10325476; } MD5_STATIC void md5_append(md5_state_t *pms, const md5_byte_t *data, size_t nbytes) { const md5_byte_t *p = data; size_t left = nbytes; size_t offset = (pms->count[0] >> 3) & 63; md5_word_t nbits = (md5_word_t)(nbytes << 3); if (nbytes <= 0) return; /* Update the message length. */ pms->count[1] += (md5_word_t)(nbytes >> 29); pms->count[0] += nbits; if (pms->count[0] < nbits) pms->count[1]++; /* Process an initial partial block. */ if (offset) { size_t copy = (offset + nbytes > 64 ? 64 - offset : nbytes); memcpy(pms->buf + offset, p, copy); if (offset + copy < 64) return; p += copy; left -= copy; md5_process(pms, pms->buf); } /* Process full blocks. */ for (; left >= 64; p += 64, left -= 64) md5_process(pms, p); /* Process a final partial block. */ if (left) memcpy(pms->buf, p, left); } MD5_STATIC void md5_finish(md5_state_t *pms, md5_byte_t digest[16]) { static const md5_byte_t pad[64] = {0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; md5_byte_t data[8]; int i; /* Save the length before padding. */ for (i = 0; i < 8; ++i) data[i] = (md5_byte_t)(pms->count[i >> 2] >> ((i & 3) << 3)); /* Pad to 56 bytes mod 64. */ md5_append(pms, pad, ((55 - (pms->count[0] >> 3)) & 63) + 1); /* Append the length. */ md5_append(pms, data, 8); for (i = 0; i < 16; ++i) digest[i] = (md5_byte_t)(pms->abcd[i >> 2] >> ((i & 3) << 3)); } /* End of md5.inl */ webfakes/src/cleancall.c0000644000176200001440000001017214323222672014716 0ustar liggesusers#define R_NO_REMAP #include #include "cleancall.h" #if (defined(R_VERSION) && R_VERSION < R_Version(3, 4, 0)) SEXP R_MakeExternalPtrFn(DL_FUNC p, SEXP tag, SEXP prot) { fn_ptr ptr; ptr.fn = p; return R_MakeExternalPtr(ptr.p, tag, prot); } DL_FUNC R_ExternalPtrAddrFn(SEXP s) { fn_ptr ptr; ptr.p = R_ExternalPtrAddr(s); return ptr.fn; } #endif // The R API does not have a setter for function pointers SEXP cleancall_MakeExternalPtrFn(DL_FUNC p, SEXP tag, SEXP prot) { fn_ptr tmp; tmp.fn = p; return R_MakeExternalPtr(tmp.p, tag, prot); } void cleancall_SetExternalPtrAddrFn(SEXP s, DL_FUNC p) { fn_ptr ptr; ptr.fn = p; R_SetExternalPtrAddr(s, ptr.p); } // Initialised at load time with the `.Call` primitive SEXP cleancall_fns_dot_call = NULL; void cleancall_init(void) { cleancall_fns_dot_call = Rf_findVar(Rf_install(".Call"), R_BaseEnv); } struct eval_args { SEXP call; SEXP env; }; static SEXP eval_wrap(void* data) { struct eval_args* args = (struct eval_args*) data; return Rf_eval(args->call, args->env); } SEXP cleancall_call(SEXP args, SEXP env) { SEXP call = PROTECT(Rf_lcons(cleancall_fns_dot_call, args)); struct eval_args data = { call, env }; SEXP out = r_with_cleanup_context(&eval_wrap, &data); UNPROTECT(1); return out; } static SEXP callbacks = NULL; // Preallocate a callback static void push_callback(SEXP stack) { SEXP top = CDR(stack); SEXP early_handler = PROTECT(Rf_allocVector(LGLSXP, 1)); SEXP fn_extptr = PROTECT(cleancall_MakeExternalPtrFn(NULL, R_NilValue, R_NilValue)); SEXP data_extptr = PROTECT(R_MakeExternalPtr(NULL, early_handler, R_NilValue)); SEXP cb = Rf_cons(Rf_cons(fn_extptr, data_extptr), top); SETCDR(stack, cb); UNPROTECT(3); } struct data_wrapper { SEXP (*fn)(void* data); void *data; SEXP callbacks; int success; }; static void call_exits(void* data) { // Remove protecting node. Don't remove the preallocated callback on // the top as it might contain a handler when something went wrong. SEXP top = CDR(callbacks); // Restore old stack struct data_wrapper* state = data; callbacks = (SEXP) state->callbacks; // Handlers should not jump while (top != R_NilValue) { SEXP cb = CAR(top); top = CDR(top); void (*fn)(void*) = (void (*)(void*)) R_ExternalPtrAddrFn(CAR(cb)); void *data = (void*) R_ExternalPtrAddr(CDR(cb)); int early_handler = LOGICAL(R_ExternalPtrTag(CDR(cb)))[0]; // Check for empty pointer in preallocated callbacks if (fn) { if (!early_handler || !state->success) fn(data); } } } static SEXP with_cleanup_context_wrap(void *data) { struct data_wrapper* cdata = data; SEXP ret = cdata->fn(cdata->data); cdata->success = 1; return ret; } SEXP r_with_cleanup_context(SEXP (*fn)(void* data), void* data) { // Preallocate new stack before changing `callbacks` to avoid // leaving the global variable in a bad state if alloc fails SEXP new = PROTECT(Rf_cons(R_NilValue, R_NilValue)); push_callback(new); if (!callbacks) callbacks = R_NilValue; SEXP old = callbacks; callbacks = new; struct data_wrapper state = { fn, data, old, 0 }; SEXP out = R_ExecWithCleanup(with_cleanup_context_wrap, &state, &call_exits, &state); UNPROTECT(1); return out; } static void call_save_handler(void (*fn)(void *data), void* data, int early) { if (!callbacks) { fn(data); Rf_error("Internal error: Exit handler pushed outside " "of an exit context"); } SEXP cb = CADR(callbacks); // Update pointers cleancall_SetExternalPtrAddrFn(CAR(cb), (DL_FUNC) fn); R_SetExternalPtrAddr(CDR(cb), data); LOGICAL(R_ExternalPtrTag(CDR(cb)))[0] = early; // Preallocate the next callback in case the allocator jumps push_callback(callbacks); } void r_call_on_exit(void (*fn)(void* data), void* data) { call_save_handler(fn, data, /* early = */ 0); } void r_call_on_early_exit(void (*fn)(void* data), void* data) { call_save_handler(fn, data, /* early = */ 1); } webfakes/NAMESPACE0000644000176200001440000000164214740243712013267 0ustar liggesusers# Generated by roxygen2: do not edit by hand S3method(format,webfakes_app) S3method(format,webfakes_app_process) S3method(format,webfakes_regexp) S3method(format,webfakes_request) S3method(format,webfakes_response) S3method(print,webfakes_app) S3method(print,webfakes_app_process) S3method(print,webfakes_regexp) S3method(print,webfakes_request) S3method(print,webfakes_response) export(git_app) export(http_time_stamp) export(httpbin_app) export(local_app_process) export(mw_cgi) export(mw_cookie_parser) export(mw_etag) export(mw_json) export(mw_log) export(mw_multipart) export(mw_range_parser) export(mw_raw) export(mw_static) export(mw_text) export(mw_urlencoded) export(new_app) export(new_app_process) export(new_regexp) export(oauth2_httr_login) export(oauth2_login) export(oauth2_resource_app) export(oauth2_third_party_app) export(server_opts) export(tmpl_glue) useDynLib(webfakes, .registration = TRUE, .fixes = "c_") webfakes/LICENSE0000644000176200001440000000005614740243712013053 0ustar liggesusersYEAR: 2023 COPYRIGHT HOLDER: webfakes authors webfakes/NEWS.md0000644000176200001440000000445214740433460013151 0ustar liggesusers# webfakes 1.3.2 * New server option: `decode_url`. If set to `FALSE`, then the web server will not URL-decode the URL (#106). # webfakes 1.3.1 No changes. # webfakes 1.3.0 * New `git_app()` app to fake a git HTTP server. See the webfakes test cases for examples. * New `mw_cgi()` middleware to call CGI scripts. See the new `git_app()` for an example. # webfakes 1.2.1 * `tmpl_glue()` now works correctly on platforms with an issue in `readChar(..., useBytes = TRUE)`, e.g. on macOS 14.x Sonoma: . # webfakes 1.2.0 * The httpbin app now implements the `/brotli`, `/deflate`, `/digest-auth` `/forms/post`, `/hidden-basic-auth`, `/range/:n`, `/stream/:n`, `/cache` and `/cache/:value` endpoints. With these, it implements all endpoint of the original Python httpbin app (#3). * New middleware `mw_cookie_parser()` to parse a `Cookie` header. Relatedly, new `response$add_cookie()` and `response$clear_cookie()` methods to add a cookie to a response and to add a header that clears a cookie (#2). * Parsing query parametes without a value now does not fail. * New utility function `http_time_stamp()` to format a time stamp for HTTP. * The httpbin app now implements the endpoints related to cookies (#3). * The httpbin app now sends the `Date` header in the correct format. * The `offset` parameter is now optional in the `/links` endpoint of the httpbin app. * `mw_etag()` now does not add an `ETag` header to the response, if there is one already. (The comparision is case sensitive.) * New middleware: `mw_range_parser()` to parse `Range` headers. # webfakes 1.1.7 * No user visible changes. # webfakes 1.1.6 * `response$send_file()` now handles `root = "/"` and absolute paths better on Windows. * `new_app_process()` and `local_app_process()` are now faster, because the app object they need to copy to the subprocess is smaller. # webfakes 1.1.5 * `mw_etag()` now handles the `If-None-Match` header properly, and sets the status code of the response to 304, and removes the response body. # webfakes 1.1.4 * No user visible changes. # webfakes 1.1.3 * webfakes now compiles on older macOS versions, hopefully really. # webfakes 1.1.2 * webfakes now compiles on older macOS versions (before 10.12). # 1.1.1 First release on CRAN webfakes/inst/0000755000176200001440000000000014172041777013030 5ustar liggesuserswebfakes/inst/views/0000755000176200001440000000000014172041777014165 5ustar liggesuserswebfakes/inst/views/authorize.html0000644000176200001440000000072314172041777017067 0ustar liggesusers Webfakes OAuth 2.0 resource server

Would you authorize "{app}" to access your account?

webfakes/inst/credits/0000755000176200001440000000000014172041777014465 5ustar liggesuserswebfakes/inst/credits/ciwetweb.md0000644000176200001440000000750214172041777016624 0ustar liggesusers This is from https://github.com/civetweb/civetweb/blob/master/CREDITS.md # Civetweb Contributors * Abhishek Lekshmanan * Abramo Bagnara * Adam Bailey * Alan Somers * Alberto Bignotti * Alex Kozlov * AndreyArsov * Anton Te * beaver * bel2125 * Ben M. Ward * Bernhard Lehner * BigJoe * Bjoern Petri * Braedy Kuzma * Breno Ramalho Lemes * brett * Brian Lambert * Brian Spratke * cdbishop * celeron55 * Charles Olivi * Chris Han * Chris Jones * Chris Rehn * Christian Mauderer * Christopher Galas * cjh * Colden Cullen * Colm Sloan * Cortronic * Daniel Oaks * Daniel Rempel * Danny Al-Gaaf * Dave Brower * daveelton * David Arnold * David Loffredo * Dialga * Domenico Di Iorio * Drew Wells * duong2179 * ehlertjd * eugene * Eric Tsau * Erick Vieyra * Erik Beran * Erik Partridge * extergnoto * F-Secure Corporation * Fabrice Fontaine * feneuilflo * Fernando G. Aranda * Frank Hilliger * Grahack * Gregor Jasny * grenclave * grunk * guangqing.chen * Guilherme Amadio * hansipie * HariKamath Kamath * Henry Chang * Herumb Shandilya * Herve Codina * Iain Morton * ImgBotApp * Ivan Dlugos * Jack * Jacob Repp * Jacob Skillin * Jan Kowalewski * Jan Willem Janssen * Jeremy Lin * Jesse Williamson * Jim Evans * jmc- * Joakim L. Gilje * Jochen Scheib * Joe Mucchiello * Joel Gallant * Johan De Taeye * John Faith * Jordan * Jordan Shelley * Joshua Boyd * Joshua D. Boyd * k-mds * kakwa * kalphamon * Karol Mroz * Keith Holman * Keith Kyzivat * Ken Walters * Kevin Branigan * Kevin Wojniak * Khem Raj * Kimmo Mustonen * Krzysztof Kozlowski * Lammert Bies * Lars Immisch * Lawrence * Li Peng * Lianghui * Luka Rahne * Lukas Martanovic * Maarten Fremouw * makrsmark * marco * Mark Lakata * Martin Gaida * Mateusz Gralka * Matt Clarkson * Mellnik * Mike Crowe * mingodad * Morgan McGuire * mrdvlpr.xnu * Nat! * Neil Jensen * newsoft * nfrmtkr * Nick Hildebrant * Nigel Stewart * nihildeb * No Face Press * palortoff * Patrick Drechsler * Patrick Trinkle * Paul Sokolovsky * Paulo Brizolara * pavel.pimenov * PavelVozenilek * Perttu Ahola * Peter Foerster * Philipp Friedenberger * Philipp Hasper * Piotr Zierhoffer * pkvamme * Radoslaw Zarzynski * Red54 * Retallack Mark mark.retallack * Richard Screene * Rimas Misevi-ìius * Rinat Dobrokhotov * ryankopf * Sage Weil * Sangwhan Moon * Saumitra Vikram * Scott Nations * Sebastien Jodogne * Sergey Linev * sgmesservey * shantanugadgil * Sherwyn Sen * shreyajaggi8 * Simon Hailes * slidertom * SpaceLord * sunfch * suzukibitman * Símal Rasmussen * Tamotsu Kanoh * thewaterymoon * Thiago Macedo * THILMANT, Bernard * Thomas Davis * Thomas Klausner * Thorsten Horstmann * tnoho * Tom Deblauwe * Tomas Andrle * Tomasz Gorochowik * Toni Wilk * Torben Jonas * Uilian Ries * Ulrich Hertlein * Walt Steverson * wangli28 * webxer * William Greathouse * xeoshow * xtne6f * Yehuda Sadeh * Yury Z * zhen.wang and others. # Mongoose Contributors CivetWeb is based on the Mongoose code. The following users contributed to the original Mongoose release between 2010 and 2013. This list was generated from the Mongoose GIT logs. It does not contain contributions from the Mongoose mailing list. There is no record for contributors prior to 2010. * Sergey Lyubka * Arnout Vandecappelle (Essensium/Mind) * Benoît Amiaux * Cody Hanson * Colin Leitner * Daniel Oaks * Eric Bakan * Erik Oomen * Filipp Kovalev * Ger Hobbelt * Hendrik Polczynski * Henrique Mendonça * Igor Okulist * Jay * Joe Mucchiello * John Safranek * Joseph Mainwaring * José Miguel Gonçalves * KIU Shueng Chuan * Katerina Blinova * Konstantin Sorokin * Marin Atanasov Nikolov * Matt Healy * Miguel Morales * Mikhail Nikalyukin * MikieMorales * Mitch Hendrickson * Nigel Stewart * Pavel * Pavel Khlebovich * Rogerz Zhang * Sebastian Reinhard * Stefan Doehla * Thileepan * abadc0de * arvidn * bick * ff.feng * jmucchiello * jwang * lsm * migal * mlamb * nullable.type * shantanugadgil * tayS * test * valenok webfakes/inst/credits/redoc.md0000644000176200001440000001333414172041777016107 0ustar liggesusers See https://github.com/Redocly/redoc#readme for Redoc. Redoc authors: * Roman Hotsiy * Cesar * Dimitar Nanov * Ivan Goncharov * Anya Stasiuk * Bohdan Khorolets * Brendan Abbott * Ben Firshman * brushmate * Adam Altman * Jérémy Derussé * Matthias Mohr * Mike Stead * kedashoe * leoliu * amanganiello * Patrick Elam <40776618+patrickelam@users.noreply.github.com> * Mohamed Zenadi * Sergey Dubovyk * Zakary Kamal Ismail * Jon Nicholson * Alex * TATSUNO Yasuhiro * Oleksiy Kachynskyy * Mike Ralphson * Phil Sturgeon * Dimitar Nanov * Faheem Abrar * Fredrik Lengstrand <10126466+fredriklengstrand@users.noreply.github.com> * Gaurav Jain * George Wilson * GreenHedgehog * Homa Wong * Ingo Claro * Jacob Baskin * Jean-Daniel * Joe Honzawa * Josh Price * Julian <2564520+julmuell@users.noreply.github.com> * Julian Kühnel * Julien Feltesse * Khoa Tran * Kris Kalavantavanich * Kryštof Korb * LeFnord * Leonard Ehrenfried * Lev Pachmanov <31389480+levpachmanov@users.noreply.github.com> * Luciano Jr * Luigi Pinca * Making GitHub Delicious * Marius Rumpf * Mason Malone * Mathias Schreck * Melvyn Sopacua <40824897+mes3yd@users.noreply.github.com> * Melvyn Sopacua * Michael Huynh <43751307+miqh@users.noreply.github.com> * Morgan Terry * Nan Yan <625518543@qq.com> * Nick Oliver * Oleg Vaskevich * Patrick Niklaus * Patrick Rodacker * Pete Nykänen * Peter Golm * Peter Wessels * Petr Flaks * Primož Pincolič * Quinn Blenkinsop * Robert DeRose * Sander van de Graaf * Sebastián Ramírez * Sergio Regueira * SeungWoon Maz Lee * Sheila Kelly <36901025+viralanomaly@users.noreply.github.com> * Siarhei Bautrukevich * Skyler Lewis * SoftBrix * Sérgio Ramos * Tim Hordern * Vincent Giersch * Vladimir L * William Boman * Zach Pomerantz * bwjohnson-ss <41123899+bwjohnson-ss@users.noreply.github.com> * davchen51 <46250804+davchen51@users.noreply.github.com> * duxiaofeng * fritz-c * jsmartfo * kmbenitez * lrobledo * lscholten * mknoszlig * neumond * pengfluf * russellrobinson * tomjankes * torbenw * travis@localhost * unarist * zhzxang * Adam Altman * Adam DuVander * Adrien Gallou * Aleksandr Karo * Alex Scammon * Anastasiya Mashoshyna * Andrew Berry * Andrew Zhukevych <34597767+Hollister009@users.noreply.github.com> * Andrii Tykhan * Antherkiv * Anthony Porthouse * Anto * Anton Komarev <1849174+antonkomarev@users.noreply.github.com> * Benny Neugebauer * Blake Erickson * Brendan Abbott * Cesar Landeros Delgado * Cesar Level * Chris Faulkner * Cédric Bertolini <41571181+wizacedric@users.noreply.github.com> * Cédric Fabianski * Daniel Chao * Dave Oram * David Beacham * David Cumps * David J. Felix * David Revay * DeBr0glie The authors were extracted from the git history: ```sh git clone https://github.com/Redocly/redoc.git cd redoc git shortlog --summary --numbered --email --all | cut -f2 | sed 's/^/* /' ``` webfakes/inst/examples/0000755000176200001440000000000014172041777014646 5ustar liggesuserswebfakes/inst/examples/hello/0000755000176200001440000000000014172041777015751 5ustar liggesuserswebfakes/inst/examples/hello/views/0000755000176200001440000000000014172041777017106 5ustar liggesuserswebfakes/inst/examples/hello/views/test.txt0000644000176200001440000000003114172041777020620 0ustar liggesusersThis is {2-1} glue test! webfakes/inst/examples/hello/app.R0000644000176200001440000000070514172041777016656 0ustar liggesusers library(webfakes) app <- new_app() app$engine("txt", tmpl_glue()) app$use(mw_log()) app$get("/hello", function(req, res) { res$send("Hello there!") }) app$get(new_regexp("^/hi(/.*)?$"), function(req, res) { res$send("Hi indeed!") }) app$post("/hello", function(req, res) { res$send("Got it, thanks!") }) app$get("/view", function(req, res) { txt <- res$render("test") res$ set_type("text/plain")$ send(txt) }) app$listen(3000L) webfakes/inst/examples/httpbin/0000755000176200001440000000000014740243712016310 5ustar liggesuserswebfakes/inst/examples/httpbin/assets/0000755000176200001440000000000014740243712017612 5ustar liggesuserswebfakes/inst/examples/httpbin/assets/httpbin.html0000644000176200001440000127130614740243712022162 0ustar liggesusers webfakes::httpbin_app() API

webfakes::httpbin_app() API (1.0.0)

Download OpenAPI specification:Download

This is a webfakes web app for HTTP testing in R packages. It implements the https://httpbin.org API

HTTP methods

GET request

An endpoint to make a GET request agains. It returns the request's parameters and the request headers, in JSON.

Responses

Response samples

Content type
application/json
{
  • "args": { },
  • "headers": { },
  • "origin": "string",
  • "path": "string",
  • "url": "string"
}

DELETE request

Endpoint to make DELETE requests againts.

Responses

Response samples

Content type
application/json
{
  • "args": { },
  • "data": { },
  • "files": { },
  • "form": { },
  • "headers": { },
  • "json": { },
  • "method": "string",
  • "path": "string",
  • "origin": "string",
  • "url": "string"
}

PATCH request

Endpoint to make PATCH requests against.

Responses

Response samples

Content type
application/json
{
  • "args": { },
  • "data": { },
  • "files": { },
  • "form": { },
  • "headers": { },
  • "json": { },
  • "method": "string",
  • "path": "string",
  • "origin": "string",
  • "url": "string"
}

POST request

Endpoints to make a POST request against.

Responses

Response samples

Content type
application/json
{
  • "args": { },
  • "data": { },
  • "files": { },
  • "form": { },
  • "headers": { },
  • "json": { },
  • "method": "string",
  • "path": "string",
  • "origin": "string",
  • "url": "string"
}

PUT request

Endpoints to make a PUT request against.

Responses

Response samples

Content type
application/json
{
  • "args": { },
  • "data": { },
  • "files": { },
  • "form": { },
  • "headers": { },
  • "json": { },
  • "method": "string",
  • "path": "string",
  • "origin": "string",
  • "url": "string"
}

Auth

Basic authentication.

Basic authentication with specified user and password

path Parameters
user
required
string

User name.

password
required
string

Password.

header Parameters
Authorization
string

Base64 encoded user name and password. See https://en.wikipedia.org/wiki/Basic_access_authentication#Client_side for the protocol.

Responses

Response samples

Content type
application/json
{
  • "authenticated": true,
  • "user": "Aladdin"
}

Hidden basic authentication.

Basic authentication with specified user and password. This is the same as /basic-auth, but it returns a 404 if no authentication is supplied or the authentication fails.

path Parameters
user
required
string

User name.

password
required
string

Password.

header Parameters
Authorization
string

Base64 encoded user name and password. See https://en.wikipedia.org/wiki/Basic_access_authentication#Client_side for the protocol.

Responses

Response samples

Content type
application/json
{
  • "authenticated": true,
  • "user": "Aladdin"
}

Digest authentication.

Digest authentication with specified user and password

path Parameters
qop
required
string

Quality of protection, possible values are auth and auth-int.

user
required
string

User name.

passwd
required
string

Password.

query Parameters
require_cookie
any

Whether to require cookie handling for a successful authentication. Set to 1, 'tortrue` to require cookies.

header Parameters
Authorization
string

Digest authentication header. See https://en.wikipedia.org/wiki/Digest_access_authentication for the details.

Responses

Response samples

Content type
application/json
{
  • "authenticated": true,
  • "user": "Aladdin"
}

Digest authentication algorithms.

Digest authentication with specified user and password, and algorithm.

path Parameters
qop
required
string

Quality of protection, possible values are auth and auth-int.

user
required
string

User name.

passwd
required
string

Password.

algorithm
required
string

Hashing algorithm to use: MD5, SHA-256 or SHA-512.

query Parameters
require_cookie
any

Whether to require cookie handling for a successful authentication. Set to 1, 'tortrue` to require cookies.

header Parameters
Authorization
string

Digest authentication header. See https://en.wikipedia.org/wiki/Digest_access_authentication for the details.

Responses

Response samples

Content type
application/json
{
  • "authenticated": true,
  • "user": "Aladdin"
}

Digest authentication, stale after.

Digest authentication with specified user, password and algorithm, with a stale_after value.

path Parameters
qop
required
string

Quality of protection, possible values are auth and auth-int.

user
required
string

User name.

passwd
required
string

Password.

algorithm
required
string

Hashing algorithm to use: MD5, SHA-256 or SHA-512.

stale_after
required
string

How many requests are allowed. This is stored in a cookie.

query Parameters
require_cookie
any

Whether to require cookie handling for a successful authentication. Set to 1, 'tortrue` to require cookies.

header Parameters
Authorization
string

Digest authentication header. See https://en.wikipedia.org/wiki/Digest_access_authentication for the details.

Responses

Response samples

Content type
application/json
{
  • "authenticated": true,
  • "user": "Aladdin"
}

Bearer authentication.

Checks is that a token is supplied in the Authorization header.

header Parameters
Authorization
string^Bearer

Responses

Response samples

Content type
application/json
{
  • "authenticated": true,
  • "token": "string"
}

Status codes

Return the specified HTTP status code, works for all HTTP verbs.

This endpoint works for these other HTTP verbs as well, the same way as for GET: CONNECT, DELETE, HEAD, MKCOL, OPTIONS, PATCH, POST, PROPFIND, PUT, REPORT. See https://en.wikipedia.org/wiki/List_of_HTTP_status_codes for more about status codes.

path Parameters
status
required
integer

Status code.

Responses

Return the specified HTTP status code, works for all HTTP verbs.

This endpoint works for these other HTTP verbs as well, the same way as for GET: CONNECT, DELETE, HEAD, MKCOL, OPTIONS, PATCH, POST, PROPFIND, PUT, REPORT. See https://en.wikipedia.org/wiki/List_of_HTTP_status_codes for more about status codes.

path Parameters
status
required
integer

Status code.

Responses

Return the specified HTTP status code, works for all HTTP verbs.

This endpoint works for these other HTTP verbs as well, the same way as for GET: CONNECT, DELETE, HEAD, MKCOL, OPTIONS, PATCH, POST, PROPFIND, PUT, REPORT. See https://en.wikipedia.org/wiki/List_of_HTTP_status_codes for more about status codes.

path Parameters
status
required
integer

Status code.

Responses

Return the specified HTTP status code, works for all HTTP verbs.

This endpoint works for these other HTTP verbs as well, the same way as for GET: CONNECT, DELETE, HEAD, MKCOL, OPTIONS, PATCH, POST, PROPFIND, PUT, REPORT. See https://en.wikipedia.org/wiki/List_of_HTTP_status_codes for more about status codes.

path Parameters
status
required
integer

Status code.

Responses

Return the specified HTTP status code, works for all HTTP verbs.

This endpoint works for these other HTTP verbs as well, the same way as for GET: CONNECT, DELETE, HEAD, MKCOL, OPTIONS, PATCH, POST, PROPFIND, PUT, REPORT. See https://en.wikipedia.org/wiki/List_of_HTTP_status_codes for more about status codes.

path Parameters
status
required
integer

Status code.

Responses

Request inspection

HTTP request headers.

Responses

Response samples

Content type
application/json
{
  • "headers": { }
}

IP address of the client.

Responses

Response samples

Content type
application/json
{
  • "origin": "string"
}

The client's user agent.

Responses

Response samples

Content type
application/json
{
  • "user-agent": "string"
}

Response inspection

Caching

Returns a 304 if an If-Modified-Since header or If-None-Match is present. Returns the same as a GET otherwise.

header Parameters
If-Modified-Since
any
If-None-Match
any

Responses

Cache control.

Sets a Cache-Control header for n seconds.

path Parameters
value
integer

Responses

Work with ETags.

Assumes the specified etag parameter as the ETag header.

  • If a matching tag is supplied in the If-None-Match header, then it returns with HTTP status code 304.
  • If a non-matching If-Match header is supplied, then it return with HTTP status code 412.
  • Otherwise it returns with a JSON body and status 200.

See https://en.wikipedia.org/wiki/HTTP_ETag for more about ETag headers.

path Parameters
etag
required
string

Assumed ETag value.

Responses

Response samples

Content type
application/json
{
  • "args": { },
  • "headers": { },
  • "origin": "string",
  • "path": "string",
  • "url": "string"
}

Set response headers.

Set response headers from the passed query parameters. The same parameter migth be specified multiple times to create the same HTTP header multiple times.

query Parameters
object

Responses

Response samples

Content type
application/json
{ }

Set response headers.

Set response headers from the passed query parameters. The same parameter migth be specified multiple times to create the same HTTP header multiple times.

query Parameters
object

Responses

Response samples

Content type
application/json
{ }

Response formats

A simple page that is denied for robots.

This exists in conjunction with /robots.txt which disallows this page.

Responses

Send gzip encoded data.

Responses

Response samples

Content type
application/json
{
  • "args": { },
  • "data": { },
  • "files": { },
  • "form": { },
  • "headers": { },
  • "json": { },
  • "method": "string",
  • "path": "string",
  • "origin": "string",
  • "url": "string"
}

Deflated data.

Returns Deflate-encoded data. This needs the zip R package.

brotli compression

Returns brotli-encoded data. This needs the brotli R package.

Send UTF-8 enconded data.

Responses

Send an HTML page.

Responses

Send a JSON document.

Responses

Response samples

Content type
application/json
{ }

Send example `robots.txt` rules.

It will not allow the /deny endpoint.

Responses

Send an XML document.

Responses

Dynamic data

Base64 decoder

Base64 decode the supplied value and echo it back.

path Parameters
value
required
string

Assumed ETag value.

Responses

Random bytes.

path Parameters
n
required
integer

Number of bytes, maximum 10000.

Responses

Delayed response.

Wait for the specified number of seconds before sending the response.

path Parameters
secs
required
number

Number of seconds, fractions are supported.

Responses

Response samples

Content type
application/json
{
  • "args": { },
  • "data": { },
  • "files": { },
  • "form": { },
  • "headers": { },
  • "json": { },
  • "method": "string",
  • "path": "string",
  • "origin": "string",
  • "url": "string"
}

Drip bytes

Drip the specified number of bytes over the specified number of seconds, potentially after some initial delay.

query Parameters
duration
number
Default: 2

Number of seconds for the whole response.

numbytes
integer
Default: 10

Number of bytes to return.

code
integer
Default: 200

HTTP status code to return with.

delay
number
Default: 0

Initial delay, in seconds. Zero means no delay.

Responses

Links

Generate a page containing n links to other pages which do the same.

path Parameters
n
required
integer

Number of links.

offset
integer

Id of the page to return, out of n.

Responses

Stream JSON chunks.

Stream n JSON responses, each terminated by a newline character.

path Parameters
n
required
integer

Number of JSON chunks to send.

Responses

Stream in chunks.

Streams n random bytes generated with given seed, at given chunk size per packet.

path Parameters
n
required
integer

Number of bytes, maximum 102400.

query Parameters
seed
number
Default: 42

Random seed.

chunk-size
integer
Default: 10240

Size of a chunked for the chunked encoding. The last chunk migh be shorter than his.

Responses

Range requests.

Send only part of the response, after a Range header.

path Parameters
n
required
integer

Number of bytes, maximum 102400.

query Parameters
chunk_size
number
Default: 10240

Chunk size, if duration is not 0.

duration
number
Default: 0

Duration for the total response. It applies to the full response size without ranges.

Responses

Range requests..

path Parameters
n
required
integer

Number of bytes, maximum 102400.

query Parameters
chunk_size
number
Default: 10240

Chunk size, if duration is not 0.

duration
number
Default: 0

Duration for the total response. It applies to the full response size without ranges.

Responses

Random UUID.

It is pseudo-random, and not secure.

Responses

Response samples

Content type
application/json
{
  • "uuid": "ee4610df-3b7b-4624-8ce9-da6975b3e9f1"
}

Cookies

Cookie data.

Returns cookie data.

Responses

Response samples

Content type
application/json
{
  • "cookies": { }
}

Delete cookies.

Deletes cookie(s) as provided by the query string and redirects to cookie list.

query Parameters
freeform
any

Responses

Set cookies.

Sets cookie(s) as provided by the query string and redirects to cookie list.

query Parameters
freeform
any

Responses

Set one cookie.

Sets a cookie and redirects to cookie list.

path Parameters
name
string

Cookie name.

value
string

Cookie value.

Responses

Images

Return an image.

It selects the format according to the Accept request header. Supported formats: image/jpeg, image/png, image/svg+xml, image/webp, image/*.

header Parameters
Accept
string

Accepted image content type.

Responses

Response samples

Content type
application/json
{
  • "message": "Client did not request a supported media type.",
  • "accept": [
    ]
}

Return an image of the specified format.

path Parameters
format
required
string

Image format. May be jpeg, png, svg or webp.

Responses

Redirects

Redirect to an absolute URL, n times.

path Parameters
n
required
integer

Number of times to redirect. Maximum is five.

Responses

Redirect to a relative URL, n times.

path Parameters
n
required
integer

Number of times to redirect. Maximum is five.

Responses

Redirect to an URL.

See https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#3xx_Redirection for details. This endpoint works for these other HTTP verbs as well, the same way as for GET: CONNECT, DELETE, HEAD, MKCOL, OPTIONS, PATCH, POST, PROPFIND, PUT, REPORT.

query Parameters
url
string

URL to redirect to.

status_code
integer
Default: 302

HTTP status code to use for the redirection.

Responses

Redirect to an URL.

See https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#3xx_Redirection for details. This endpoint works for these other HTTP verbs as well, the same way as for GET: CONNECT, DELETE, HEAD, MKCOL, OPTIONS, PATCH, POST, PROPFIND, PUT, REPORT.

query Parameters
url
string

URL to redirect to.

status_code
integer
Default: 302

HTTP status code to use for the redirection.

Responses

Redirect to an URL.

See https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#3xx_Redirection for details. This endpoint works for these other HTTP verbs as well, the same way as for GET: CONNECT, DELETE, HEAD, MKCOL, OPTIONS, PATCH, POST, PROPFIND, PUT, REPORT.

query Parameters
url
string

URL to redirect to.

status_code
integer
Default: 302

HTTP status code to use for the redirection.

Responses

Redirect to an URL.

See https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#3xx_Redirection for details. This endpoint works for these other HTTP verbs as well, the same way as for GET: CONNECT, DELETE, HEAD, MKCOL, OPTIONS, PATCH, POST, PROPFIND, PUT, REPORT.

query Parameters
url
string

URL to redirect to.

status_code
integer
Default: 302

HTTP status code to use for the redirection.

Responses

Redirect to an URL.

See https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#3xx_Redirection for details. This endpoint works for these other HTTP verbs as well, the same way as for GET: CONNECT, DELETE, HEAD, MKCOL, OPTIONS, PATCH, POST, PROPFIND, PUT, REPORT.

query Parameters
url
string

URL to redirect to.

status_code
integer
Default: 302

HTTP status code to use for the redirection.

Responses

Anything

Returns anything passed in request data.

This endpoint works for these other HTTP verbs as well, the same way as for GET: CONNECT, DELETE, HEAD, MKCOL, OPTIONS, PATCH, POST, PROPFIND, PUT, REPORT.

Responses

Response samples

Content type
application/json
{
  • "args": { },
  • "data": { },
  • "files": { },
  • "form": { },
  • "headers": { },
  • "json": { },
  • "method": "string",
  • "path": "string",
  • "origin": "string",
  • "url": "string"
}

Returns anything passed in request data.

This endpoint works for these other HTTP verbs as well, the same way as for GET: CONNECT, DELETE, HEAD, MKCOL, OPTIONS, PATCH, POST, PROPFIND, PUT, REPORT.

Responses

Response samples

Content type
application/json
{
  • "args": { },
  • "data": { },
  • "files": { },
  • "form": { },
  • "headers": { },
  • "json": { },
  • "method": "string",
  • "path": "string",
  • "origin": "string",
  • "url": "string"
}

Returns anything passed in request data.

This endpoint works for these other HTTP verbs as well, the same way as for GET: CONNECT, DELETE, HEAD, MKCOL, OPTIONS, PATCH, POST, PROPFIND, PUT, REPORT.

Responses

Response samples

Content type
application/json
{
  • "args": { },
  • "data": { },
  • "files": { },
  • "form": { },
  • "headers": { },
  • "json": { },
  • "method": "string",
  • "path": "string",
  • "origin": "string",
  • "url": "string"
}

Returns anything passed in request data.

This endpoint works for these other HTTP verbs as well, the same way as for GET: CONNECT, DELETE, HEAD, MKCOL, OPTIONS, PATCH, POST, PROPFIND, PUT, REPORT.

Responses

Response samples

Content type
application/json
{
  • "args": { },
  • "data": { },
  • "files": { },
  • "form": { },
  • "headers": { },
  • "json": { },
  • "method": "string",
  • "path": "string",
  • "origin": "string",
  • "url": "string"
}

Returns anything passed in request data.

This endpoint works for these other HTTP verbs as well, the same way as for GET: CONNECT, DELETE, HEAD, MKCOL, OPTIONS, PATCH, POST, PROPFIND, PUT, REPORT.

Responses

Response samples

Content type
application/json
{
  • "args": { },
  • "data": { },
  • "files": { },
  • "form": { },
  • "headers": { },
  • "json": { },
  • "method": "string",
  • "path": "string",
  • "origin": "string",
  • "url": "string"
}
webfakes/inst/examples/httpbin/assets/forms-post.html0000644000176200001440000000256614740243712022622 0ustar liggesusers

Pizza Size

Pizza Toppings

webfakes/inst/examples/httpbin/openapi.yaml0000644000176200001440000012330514740243712020633 0ustar liggesusersopenapi: 3.0.0 info: title: webfakes::httpbin_app() API version: 1.0.0 contact: email: csardi.gabor@gmail.com license: name: The MIT License url: 'https://opensource.org/licenses/MIT' description: | This is a webfakes web app for HTTP testing in R packages. It implements the https://httpbin.org API paths: /get: get: tags: - HTTP methods summary: GET request description: | An endpoint to make a GET request agains. It returns the request's parameters and the request headers, in JSON. responses: '200': description: Success. content: application/json: schema: $ref: '#/components/schemas/get' /delete: delete: tags: - HTTP methods summary: DELETE request description: | Endpoint to make DELETE requests againts. responses: '200': description: Success. content: application/json: schema: $ref: '#/components/schemas/common' /patch: patch: tags: - HTTP methods summary: PATCH request description: | Endpoint to make PATCH requests against. responses: '200': description: Success. content: application/json: schema: $ref: '#/components/schemas/common' /post: post: tags: - HTTP methods summary: POST request description: | Endpoints to make a POST request against. responses: '200': description: Success. content: application/json: schema: $ref: '#/components/schemas/common' /put: put: tags: - HTTP methods summary: PUT request description: | Endpoints to make a PUT request against. responses: '200': description: Success. content: application/json: schema: $ref: '#/components/schemas/common' '/basic-auth/:user/:password': parameters: - name: user in: path required: true description: User name. schema: type: string - name: password in: path required: true description: Password. schema: type: string - name: Authorization in: header schema: type: string description: | Base64 encoded user name and password. See for the protocol. get: tags: - Auth summary: Basic authentication. description: | Basic authentication with specified user and password responses: '200': description: Success. content: application/json: schema: type: object properties: authenticated: type: boolean user: type: string example: "Aladdin" '401': description: Failed authentication. headers: WWW-Authenticate: description: Contains the authentication realm. schema: type: string pattern: '^Basic realm=".*"' '/hidden-basic-auth/:user/:password': parameters: - name: user in: path required: true description: User name. schema: type: string - name: password in: path required: true description: Password. schema: type: string - name: Authorization in: header schema: type: string description: | Base64 encoded user name and password. See for the protocol. get: tags: - Auth summary: Hidden basic authentication. description: | Basic authentication with specified user and password. This is the same as `/basic-auth`, but it returns a 404 if no authentication is supplied or the authentication fails. responses: '200': description: Success. content: application/json: schema: type: object properties: authenticated: type: boolean user: type: string example: "Aladdin" '404': description: Failed authentication. '/digest-auth/:qop/:user/:passwd': parameters: - name: qop in: path required: true description: Quality of protection, possible values are `auth` and `auth-int`. schema: type: string - name: user in: path required: true description: User name. schema: type: string - name: passwd in: path required: true description: Password. schema: type: string - name: require_cookie in: query required: false description: | Whether to require cookie handling for a successful authentication. Set to `1`, 't` or `true` to require cookies. scheme: type: string - name: Authorization in: header schema: type: string description: | Digest authentication header. See https://en.wikipedia.org/wiki/Digest_access_authentication for the details. get: tags: - Auth summary: Digest authentication. description: | Digest authentication with specified user and password responses: '200': description: Success. content: application/json: schema: type: object properties: authenticated: type: boolean user: type: string example: "Aladdin" '401': description: Failed authentication. '/digest-auth/:qop/:user/:passwd/:algorithm': parameters: - name: qop in: path required: true description: Quality of protection, possible values are `auth` and `auth-int`. schema: type: string - name: user in: path required: true description: User name. schema: type: string - name: passwd in: path required: true description: Password. schema: type: string - name: algorithm in: path required: true description: | Hashing algorithm to use: `MD5`, `SHA-256` or `SHA-512`. schema: type: string default: MD5 - name: require_cookie in: query required: false description: | Whether to require cookie handling for a successful authentication. Set to `1`, 't` or `true` to require cookies. scheme: type: string - name: Authorization in: header schema: type: string description: | Digest authentication header. See https://en.wikipedia.org/wiki/Digest_access_authentication for the details. get: tags: - Auth summary: Digest authentication algorithms. description: | Digest authentication with specified user and password, and algorithm. responses: '200': description: Success. content: application/json: schema: type: object properties: authenticated: type: boolean user: type: string example: "Aladdin" '401': description: Failed authentication. '/digest-auth/:qop/:user/:passwd/:algorithm/:stale_after': parameters: - name: qop in: path required: true description: Quality of protection, possible values are `auth` and `auth-int`. schema: type: string - name: user in: path required: true description: User name. schema: type: string - name: passwd in: path required: true description: Password. schema: type: string - name: algorithm in: path required: true description: | Hashing algorithm to use: `MD5`, `SHA-256` or `SHA-512`. schema: type: string default: MD5 - name: stale_after in: path required: true description: | How many requests are allowed. This is stored in a cookie. schema: type: string default: 'never' - name: require_cookie in: query required: false description: | Whether to require cookie handling for a successful authentication. Set to `1`, 't` or `true` to require cookies. scheme: type: string - name: Authorization in: header schema: type: string description: | Digest authentication header. See https://en.wikipedia.org/wiki/Digest_access_authentication for the details. get: tags: - Auth summary: Digest authentication, stale after. description: | Digest authentication with specified user, password and algorithm, with a stale_after value. responses: '200': description: Success. content: application/json: schema: type: object properties: authenticated: type: boolean user: type: string example: "Aladdin" '401': description: Failed authentication. '/bearer': parameters: - name: Authorization in: header schema: type: string pattern: '^Bearer ' get: tags: - Auth summary: Bearer authentication. description: | Checks is that a token is supplied in the `Authorization` header. responses: '200': description: Success. content: application/json: schema: type: object properties: authenticated: type: boolean token: type: string '401': description: Failed authentication. headers: WWW-Authenticate: description: Contains the authentication realm. schema: type: string pattern: '^bearer$' '/status/:status': parameters: - $ref: '#/components/parameters/status' delete: tags: - Status codes summary: Return the specified HTTP status code, works for all HTTP verbs. description: | This endpoint works for these other HTTP verbs as well, the same way as for `GET`: `CONNECT`, `DELETE`, `HEAD`, `MKCOL`, `OPTIONS`, `PATCH`, `POST`, `PROPFIND`, `PUT`, `REPORT`. See for more about status codes. responses: '100': description: Informational responses. '200': description: Success. '300': description: Redirection. '400': description: Client errors. '500': description: Server errors. get: tags: - Status codes summary: Return the specified HTTP status code, works for all HTTP verbs. description: | This endpoint works for these other HTTP verbs as well, the same way as for `GET`: `CONNECT`, `DELETE`, `HEAD`, `MKCOL`, `OPTIONS`, `PATCH`, `POST`, `PROPFIND`, `PUT`, `REPORT`. See for more about status codes. responses: '100': description: Informational responses. '200': description: Success. '300': description: Redirection. '400': description: Client errors. '500': description: Server errors. patch: tags: - Status codes summary: Return the specified HTTP status code, works for all HTTP verbs. description: | This endpoint works for these other HTTP verbs as well, the same way as for `GET`: `CONNECT`, `DELETE`, `HEAD`, `MKCOL`, `OPTIONS`, `PATCH`, `POST`, `PROPFIND`, `PUT`, `REPORT`. See for more about status codes. responses: '100': description: Informational responses. '200': description: Success. '300': description: Redirection. '400': description: Client errors. '500': description: Server errors. post: tags: - Status codes summary: Return the specified HTTP status code, works for all HTTP verbs. description: | This endpoint works for these other HTTP verbs as well, the same way as for `GET`: `CONNECT`, `DELETE`, `HEAD`, `MKCOL`, `OPTIONS`, `PATCH`, `POST`, `PROPFIND`, `PUT`, `REPORT`. See for more about status codes. responses: '100': description: Informational responses. '200': description: Success. '300': description: Redirection. '400': description: Client errors. '500': description: Server errors. put: tags: - Status codes summary: Return the specified HTTP status code, works for all HTTP verbs. description: | This endpoint works for these other HTTP verbs as well, the same way as for `GET`: `CONNECT`, `DELETE`, `HEAD`, `MKCOL`, `OPTIONS`, `PATCH`, `POST`, `PROPFIND`, `PUT`, `REPORT`. See for more about status codes. responses: '100': description: Informational responses. '200': description: Success. '300': description: Redirection. '400': description: Client errors. '500': description: Server errors. '/headers': get: tags: - Request inspection summary: HTTP request headers. responses: '200': description: The requests's HTTP headers. content: application/json: schema: type: object properties: headers: type: object '/ip': get: tags: - Request inspection summary: IP address of the client. responses: '200': description: | The IP address of the client, usually `127.0.0.1`, as the webfakes server runs on the localhost. content: application/json: schema: type: object properties: origin: type: string '/user-agent': get: tags: - Request inspection summary: The client's user agent. responses: '200': description: The content of the `User-Agent` HTTP request header. content: application/json: schema: type: object properties: 'user-agent': type: string '/cache': parameters: - name: If-Modified-Since in: header - name: If-None-Match in: header get: tags: - Response inspection summary: Caching description: | Returns a 304 if an `If-Modified-Since` header or `If-None-Match` is present. Returns the same as a GET otherwise. responses: '200': description: Cached response. content: application/json: $ref: '#/components/schemas/get' '304': description: Modified. '/cache/:value': parameters: - name: value in: path schema: type: integer get: tags: - Response inspection summary: Cache control. description: | Sets a `Cache-Control` header for n seconds. responses: '200': description: Cache control set. content: application/json: $ref: '#/components/schemas/get' '/etag/:etag': parameters: - name: etag in: path required: true description: Assumed ETag value. schema: type: string get: tags: - Response inspection summary: Work with ETags. description: | Assumes the specified `etag` parameter as the `ETag` header. * If a matching tag is supplied in the `If-None-Match` header, then it returns with HTTP status code 304. * If a non-matching `If-Match` header is supplied, then it return with HTTP status code 412. * Otherwise it returns with a JSON body and status 200. See for more about `ETag` headers. responses: '200': description: | Non-matching `If-Non-Match` or matching `If-Match`, or no such headers at all. content: application/json: schema: $ref: '#/components/schemas/get' headers: ETag: description: The supplied `etag` value. schema: type: string '304': description: | Matching `If-Non-Match` header. '412': description: | Non-matchinf `If-Match` header. '/response-headers': parameters: - in: query name: params schema: type: object additionalProperties: type: string get: tags: - Response inspection summary: Set response headers. description: | Set response headers from the passed query parameters. The same parameter migth be specified multiple times to create the same HTTP header multiple times. responses: '200': description: | The passed query parameters are also returned as a JSON response. content: application/json: schema: type: object post: tags: - Response inspection summary: Set response headers. description: | Set response headers from the passed query parameters. The same parameter migth be specified multiple times to create the same HTTP header multiple times. responses: '200': description: | The passed query parameters are also returned as a JSON response. content: application/json: schema: type: object '/deny': get: tags: - Response formats summary: A simple page that is denied for robots. description: | This exists in conjunction with `/robots.txt` which disallows this page. responses: '200': description: A simple page. content: text/plain: schema: type: string example: 'Example content.' '/gzip': get: tags: - Response formats summary: Send gzip encoded data. responses: '200': description: Some gzipped JSON. content: application/json: schema: $ref: '#/components/schemas/common' headers: 'Content-Encoding': description: Set to `gzip`. schema: type: string pattern: '^gzip$' '/deflate': get: tags: - Response formats summary: Deflated data. description: | Returns Deflate-encoded data. This needs the zip R package. content: application/json: schema: $ref: '#/components/schemas/common' headers: 'Content-Encoding': description: Set to `deflate`. schema: type: string pattern: '^deflate$' '/brotli': get: tags: - Response formats summary: brotli compression description: | Returns brotli-encoded data. This needs the brotli R package. content: application/json: schema: $ref: '#/components/schemas/common' headers: 'Content-Encoding': description: Set to `brotli`. schema: type: string pattern: '^brotli' '/encoding/utf8': get: tags: - Response formats summary: Send UTF-8 enconded data. responses: '200': description: HTML with a lot of UTF-8. content: text/html: schema: type: string headers: 'Content-Type': description: Set to `text/html; charset=utf-8`. schema: type: string '/html': get: tags: - Response formats summary: Send an HTML page. responses: '200': description: An example HTML page. content: text/html: schema: type: string '/json': get: tags: - Response formats summary: Send a JSON document. responses: '200': description: An example JSON document. content: application/json: schema: type: object '/robots.txt': get: tags: - Response formats summary: Send example `robots.txt` rules. description: It will not allow the `/deny` endpoint. responses: '200': description: An example `robots.txt` file. content: text/plain: schema: type: string '/xml': get: tags: - Response formats summary: Send an XML document. responses: '200': description: An example XML document. content: application/xml: schema: type: string '/base64/:value': parameters: - name: value in: path required: true description: Assumed ETag value. schema: type: string get: tags: - Dynamic data summary: Base64 decoder description: Base64 decode the supplied value and echo it back. responses: '200': description: Decoded base64. content: application/octet-stream: schema: type: string '/bytes/:n': parameters: - name: n in: path required: true description: Number of bytes, maximum 10000. schema: type: integer get: tags: - Dynamic data summary: Random bytes. responses: '200': description: Random bytes. content: application/octest-stream: schema: type: string '/delay/:secs': parameters: - name: secs in: path required: true description: Number of seconds, fractions are supported. schema: type: number get: tags: - Dynamic data summary: Delayed response. description: | Wait for the specified number of seconds before sending the response. responses: '200': description: JSON response. content: application/json: schema: $ref: '#/components/schemas/common' '/drip': parameters: - name: duration in: query description: Number of seconds for the whole response. schema: type: number default: 2 - name: numbytes in: query description: Number of bytes to return. schema: type: integer default: 10 - name: code in: query description: HTTP status code to return with. schema: type: integer default: 200 - name: delay in: query description: Initial delay, in seconds. Zero means no delay. schema: type: number default: 0 get: tags: - Dynamic data summary: Drip bytes description: | Drip the specified number of bytes over the specified number of seconds, potentially after some initial delay. responses: '200': description: | Data of the specified length. The status code is not neccesarily 200, but the one specified in the `code` query parameter. content: application/octet-stream: schema: type: string '/links/:n/:offset': parameters: - name: n in: path required: true description: Number of links. schema: type: integer - name: offset in: path required: false description: Id of the page to return, out of `n`. schema: type: integer get: tags: - Dynamic data summary: Links description: | Generate a page containing n links to other pages which do the same. responses: '200': description: | HTML links. content: text/html '/stream/:n': parameters: - name: n in: path required: true description: Number of JSON chunks to send. schema: type: integer get: tags: - Dynamic data summary: Stream JSON chunks. description: | Stream `n` JSON responses, each terminated by a newline character. responses: '200': description: | Streamed JSON responses. Each JSON chunk contains an `id` field, from zero, up to `n-1`. content: application/json '/stream-bytes/:n': parameters: - name: n in: path required: true description: Number of bytes, maximum 102400. schema: type: integer - name: seed in: query description: Random seed. schema: type: number default: 42 - name: chunk-size in: query description: | Size of a chunked for the chunked encoding. The last chunk migh be shorter than his. schema: type: integer default: 10240 get: tags: - Dynamic data summary: Stream in chunks. description: | Streams n random bytes generated with given seed, at given chunk size per packet. responses: '200': description: Random bytes. content: application/octest-stream: schema: type: string '/range/:n': parameters: - name: n in: path required: true description: Number of bytes, maximum 102400. schema: type: integer - name: chunk_size in: query required: false description: Chunk size, if `duration` is not 0. schema: type: number default: 10240 - name: duration in: query required: false description: | Duration for the total response. It applies to the full response size without ranges. schema: type: number default: 0 get: tags: - Dynamic data summary: Range requests. description: | Send only part of the response, after a `Range` header. responses: '200': description: | Full response, if there was no `Range` header, or the `Range` header was invalid. content: text/plain '206': description: | Partial response, containing the requested range. head: tags: - Dynamic data summary: Range requests.. desciption: | HEAD request to an endpoint that supports ranges. responses: '200': description: | Response with an `Accept-Ranges: bytes` header. '/uuid': get: tags: - Dynamic data summary: Random UUID. description: It is pseudo-random, and not secure. responses: '200': description: Random UUID v4 in JSON. content: application/json: schema: type: object properties: uuid: type: string example: "ee4610df-3b7b-4624-8ce9-da6975b3e9f1" '/cookies': get: tags: - Cookies summary: Cookie data. description: Returns cookie data. responses: '200': description: Set cookies. content: application/json: schema: type: object properties: cookies: type: object '/cookies/delete': parameters: - name: freeform in: query style: "form" get: tags: - Cookies summary: Delete cookies. description: | Deletes cookie(s) as provided by the query string and redirects to cookie list. responses: '302': description: Redirect to the cookie list. '/cookies/set': parameters: - name: freeform in: query style: "form" get: tags: - Cookies summary: Set cookies. description: | Sets cookie(s) as provided by the query string and redirects to cookie list. responses: '302': description: Redirect to the cookie list. '/cookies/set/:name/:value': parameters: - name: name in: path description: Cookie name. schema: type: string - name: value in: path description: Cookie value. schema: type: string get: tags: - Cookies summary: Set one cookie. description: | Sets a cookie and redirects to cookie list. responses: '302': description: Redirect to the cookie list. '/image': parameters: - name: Accept in: header description: Accepted image content type. schema: type: string get: tags: - Images summary: Return an image. description: | It selects the format according to the `Accept` request header. Supported formats: `image/jpeg`, `image/png`, `image/svg+xml`, `image/webp`, `image/*`. responses: '200': description: Image file. content: image/jpeg: schema: type: string format: binary image/png: schema: type: string format: binary image/svg+xml: schema: type: string format: binary image/webp: schema: type: string format: binary '406': description: The client did not request a supported media type. content: application/json: schema: type: object properties: message: type: string example: "Client did not request a supported media type." accept: type: array items: type: string example: - "image/jpeg" - "image/png" - "image/svg+xml" - "image/webp" '/image/:format': parameters: - name: format description: Image format. May be `jpeg`, `png`, `svg` or `webp`. in: path schema: type: string required: true get: tags: - Images summary: Return an image of the specified format. responses: '200': description: Image file. content: image/jpeg: schema: type: string format: binary image/png: schema: type: string format: binary image/svg+xml: schema: type: string format: binary image/webp: schema: type: string format: binary '/absolute-redirect/:n': parameters: - name: n in: path required: true description: Number of times to redirect. Maximum is five. schema: type: integer get: tags: - Redirects summary: Redirect to an absolute URL, n times. responses: '302': description: Redirect. '/relative-redirect/:n': parameters: - name: n in: path description: Number of times to redirect. Maximum is five. schema: type: integer required: true get: tags: - Redirects summary: Redirect to a relative URL, n times. responses: '302': description: Redirect. '/redirect-to': parameters: - name: url in: query description: URL to redirect to. schema: type: string - name: status_code in: query description: HTTP status code to use for the redirection. schema: type: integer default: 302 delete: tags: - Redirects summary: Redirect to an URL. description: | See for details. This endpoint works for these other HTTP verbs as well, the same way as for `GET`: `CONNECT`, `DELETE`, `HEAD`, `MKCOL`, `OPTIONS`, `PATCH`, `POST`, `PROPFIND`, `PUT`, `REPORT`. responses: '300': description: Multiple Choices. '301': description: Moved Permanently. '302': description: Found (Previously "Moved temporarily") '303': description: See Other (since HTTP/1.1) '304': description: Not Modified (RFC 7232) '305': description: Use Proxy (since HTTP/1.1) '306': description: Switch Proxy '307': description: Temporary Redirect (since HTTP/1.1) '308': description: Permanent Redirect (RFC 7538) get: tags: - Redirects summary: Redirect to an URL. description: | See for details. This endpoint works for these other HTTP verbs as well, the same way as for `GET`: `CONNECT`, `DELETE`, `HEAD`, `MKCOL`, `OPTIONS`, `PATCH`, `POST`, `PROPFIND`, `PUT`, `REPORT`. responses: '300': description: Multiple Choices. '301': description: Moved Permanently. '302': description: Found (Previously "Moved temporarily") '303': description: See Other (since HTTP/1.1) '304': description: Not Modified (RFC 7232) '305': description: Use Proxy (since HTTP/1.1) '306': description: Switch Proxy '307': description: Temporary Redirect (since HTTP/1.1) '308': description: Permanent Redirect (RFC 7538) patch: tags: - Redirects summary: Redirect to an URL. description: | See for details. This endpoint works for these other HTTP verbs as well, the same way as for `GET`: `CONNECT`, `DELETE`, `HEAD`, `MKCOL`, `OPTIONS`, `PATCH`, `POST`, `PROPFIND`, `PUT`, `REPORT`. responses: '300': description: Multiple Choices. '301': description: Moved Permanently. '302': description: Found (Previously "Moved temporarily") '303': description: See Other (since HTTP/1.1) '304': description: Not Modified (RFC 7232) '305': description: Use Proxy (since HTTP/1.1) '306': description: Switch Proxy '307': description: Temporary Redirect (since HTTP/1.1) '308': description: Permanent Redirect (RFC 7538) post: tags: - Redirects summary: Redirect to an URL. description: | See for details. This endpoint works for these other HTTP verbs as well, the same way as for `GET`: `CONNECT`, `DELETE`, `HEAD`, `MKCOL`, `OPTIONS`, `PATCH`, `POST`, `PROPFIND`, `PUT`, `REPORT`. responses: '300': description: Multiple Choices. '301': description: Moved Permanently. '302': description: Found (Previously "Moved temporarily") '303': description: See Other (since HTTP/1.1) '304': description: Not Modified (RFC 7232) '305': description: Use Proxy (since HTTP/1.1) '306': description: Switch Proxy '307': description: Temporary Redirect (since HTTP/1.1) '308': description: Permanent Redirect (RFC 7538) put: tags: - Redirects summary: Redirect to an URL. description: | See for details. This endpoint works for these other HTTP verbs as well, the same way as for `GET`: `CONNECT`, `DELETE`, `HEAD`, `MKCOL`, `OPTIONS`, `PATCH`, `POST`, `PROPFIND`, `PUT`, `REPORT`. responses: '300': description: Multiple Choices. '301': description: Moved Permanently. '302': description: Found (Previously "Moved temporarily") '303': description: See Other (since HTTP/1.1) '304': description: Not Modified (RFC 7232) '305': description: Use Proxy (since HTTP/1.1) '306': description: Switch Proxy '307': description: Temporary Redirect (since HTTP/1.1) '308': description: Permanent Redirect (RFC 7538) '/anything': delete: tags: - Anything summary: Returns anything passed in request data. description: | This endpoint works for these other HTTP verbs as well, the same way as for `GET`: `CONNECT`, `DELETE`, `HEAD`, `MKCOL`, `OPTIONS`, `PATCH`, `POST`, `PROPFIND`, `PUT`, `REPORT`. responses: '200': description: The request data. content: application/json: schema: $ref: '#/components/schemas/common' get: tags: - Anything summary: Returns anything passed in request data. description: | This endpoint works for these other HTTP verbs as well, the same way as for `GET`: `CONNECT`, `DELETE`, `HEAD`, `MKCOL`, `OPTIONS`, `PATCH`, `POST`, `PROPFIND`, `PUT`, `REPORT`. responses: '200': description: The request data. content: application/json: schema: $ref: '#/components/schemas/common' patch: tags: - Anything summary: Returns anything passed in request data. description: | This endpoint works for these other HTTP verbs as well, the same way as for `GET`: `CONNECT`, `DELETE`, `HEAD`, `MKCOL`, `OPTIONS`, `PATCH`, `POST`, `PROPFIND`, `PUT`, `REPORT`. responses: '200': description: The request data. content: application/json: schema: $ref: '#/components/schemas/common' post: tags: - Anything summary: Returns anything passed in request data. description: | This endpoint works for these other HTTP verbs as well, the same way as for `GET`: `CONNECT`, `DELETE`, `HEAD`, `MKCOL`, `OPTIONS`, `PATCH`, `POST`, `PROPFIND`, `PUT`, `REPORT`. responses: '200': description: The request data. content: application/json: schema: $ref: '#/components/schemas/common' put: tags: - Anything summary: Returns anything passed in request data. description: | This endpoint works for these other HTTP verbs as well, the same way as for `GET`: `CONNECT`, `DELETE`, `HEAD`, `MKCOL`, `OPTIONS`, `PATCH`, `POST`, `PROPFIND`, `PUT`, `REPORT`. responses: '200': description: The request data. content: application/json: schema: $ref: '#/components/schemas/common' components: parameters: status: name: status in: path required: true description: Status code. schema: type: integer schemas: get: properties: args: type: object headers: type: object origin: type: string path: type: string url: type: string required: - args - headers - origin - path - url common: properties: args: type: object data: type: object files: type: object form: type: object headers: type: object json: type: object method: type: string path: type: string origin: type: string url: type: string webfakes/inst/examples/httpbin/images/0000755000176200001440000000000014172041777017563 5ustar liggesuserswebfakes/inst/examples/httpbin/images/Rlogo.jpeg0000644000176200001440000001120614172041777021514 0ustar liggesusersJFIFvvC       C Ld G !"1Q#27Aaq$Bt3Wbs%Rruv2!A123QR"4aq#B ?V& :c֨j/@]Emx!+b9q|#B7a= 횥j3E9e)&'iy` s0eGr#[i2](򙏷@ s/zA ݹ]) Zz!,$ʼnI> <&2I#{4iihG@djTST̠7\}ߊ]W +0`zʚOn0xOeM.LysI vsIBW/;Uef߷#f,"CE@*k٫d^-T5q/L-C.Gq oDsWrw L&ݘ&fBy-lr߀ p{=3/EIr> )Om4TSTlL98Wpm,wg[-TM3Y(}\~ I'p/k3a"-傰wVirS{3iHEZrܹm'dPxn-zen4¦5)q#UR cQ-g:GWJ.T!!jVu]v_نM|hS0x4fǍUW |ңmCQsܵ \3݈1=:>YY[/'eϳG'y7'M)q>c 9Laz,gե\+Wِ.Ewp {|ؗle*/-7q FͶhϦ-Q-I7anFeEdWVҥ+嘂Yt`A縮8 9c{+WO~۪&sFu k*͵4tf!K)U,7<:ic!ޓgŎy?Oc2}ϬI2"Z{H^C2#?D$#$'b uyQWCSUNډ>A\jA%SQEECw Uy0UvۊhYYfxl/=]d*L<[O41;ľ p_$rU/Փ K^ZdT3BOz\j#Y8+=E-i2F'mjvW@ͫR7"+.@*Ô,UW4鴪VVo;ƪMrk u wH,yv,v*=Uܪ*!׹Tw`&/rVsd,M,oLU2J6} U`_F \dt+Eࡽ1W7,sw{ݙ(Rq\8̹0MEQ ]K!  \4pfܣ~uձ.`0Hwl7Vk>,u;{uFL wf>>b,Wӆ\ AWG*TSOJ]j+)<SkT [>`ݮsoHGS~]}>R$gVVٚ;݆ӚKySc !cc؏ iZ'f*:0.uBz+|GTfx41C` E"/&eF1k>T'}4LLLL}v4kebghАJ7U;{ӆfݭvNeGXV:n̑2Ȃxzw؋\> b BJ:MڃkiɁȒ;+op ۍSE圛sWU &QF-M|ռ7Qv UɬXI-4+x؂YIrwpFDiX%ѮzLGL. H#^b›ȓ;e\zXUVT*fC^xnLJcm6TX%n`|aOFy]2毩-1n35㨥WC5$#T'paA{nIJNE,3,(tjȵEp e2=*ŸDo>sA;^YVŒڮ.ٙMS:#+ Q4.c@kS6̳?Y3ewh؟t4;4㳇Xvzԩ KVP8 *dL>FK% e|*.e.wyx&=򞉪2v?a.H0ꈭng6d۪{f_dK"7"A:Ÿ?1M,/`^G HDV͓~=[bI6Z[T=V:^JQrC#~05A^pehh> `3$?$eJ`'CfuJHpxsY ށ2x2#}8D zB7ozM'ԯ? /MM,'Q&#wrYMaۖ0{B\PoѰBV:$ڶ;*@n4ߜ?a zUb|\7͙y ,v*nƷ6b~J^4*: aѼJPfq !-r¤ZfÌUHQ6߹0o4g筰>7)MB)R"0[:G9w"u Y%YA_{&gT+SěՆh.Qިy;rVj?.C½qE]y.ͤ|Y"9smb4cHcդys9_\WY?,.nI'h$IkԿN<˲;/G/N$9zÛ[kK_qie!q?!T0/*7)>O848ْ%S4$Bw4% oa,[K^#Q:KT;y{sdX0S.Y:]&L2w1b[GK Y"EC [؉4H<,ei~ k,&@GG6n'"QaoȫyDۛ webfakes/inst/examples/httpbin/images/Rlogo.png0000644000176200001440000002700614172041777021360 0ustar liggesusersPNG  IHDRdLbKGD pHYs.#.#x?vtIME' IDATx}w]e]O/[2d24H.(b(XAI\Z(CPB ϤM?3sgϜ$`;s2^]Yk(|S72JӴ*˲*ljv[i&l;6=aALv2EQIx1̓'Eԧff=rc>f$q&ڎ3u&۱Cu][wA4Lض u\`;&D@uiz3òCӾm'Nvwߤ6n.˅>Ѷcm۪v](7s[u] LeY( .ׅ8u8q\0YBQKh~76a9l?w_m+eEQE{}/a`[vݒzx[3uJ8 .na4EU5/)1ʋԮ]Ǫ#˴۶8(PěOnߍz,,ׅe T{-uJR$ǁm۰ٶ ۶`6,˂8@,|GnzŸLؓ2-Ӻ6 &P #`CQ޾#SUw/FTmۀ¶P8c_p]-SaIxb[lˆi 1 òUU^u ?tOj3M|۱#K( mwB"4M@ tdxRy3#aۻm8Fm{@X6,˄eZ0LeT 3oO% ?`LK øuoɞo> !0MA5 u=io?Р `g^R`;,˂i0-eum Ʋ,X 4a0=Z]U} T_bq8exJH˱x4!(2r  i.@348za0,X]*JD}Ӵ&ζ풭2*!B@34òi2M4Ų-4at]aPpU_y{VT˶INPe!Mҩ$t]E( ='x8EQ ,MӠit]aucXnS2Tq]qnS Àxa8.4Mi0b& ]aui&tW89;XpXy'L&}nSOQP8P,"N\dYqX  " y.`:TM(exb0-=h[k ^P8woE5EO=v,(ݿVg eq<\ǁ( e0m-ӄ=P)yYcE~ֶ鄐7D׃<Es8yP(/@(CQdȊ IQeo4GT ǶKIiP(B,-'CA Àa M8A4 MSjZ~tMנh}羻fCin̓ !8CYY2`5f @<G<GmM*+*P0%Q 5 #ni]Ne['s LB|}%j8}y`px׮'ˊq:N˱8E狎 # KP,C4 / 0E@4M#bb$3iE:/Cń6TW,d (EA.];cR)Bt+?c^[|JƵ7\vQضMqx<{{u\ **$Q(B\p/I_t? ;=ErLt:mUU 0i,Z(2$YBP%]]y&$i!kllgS>.ҲsL8 ˀJD$B.ȐUPͻCY4U] ?FUE>,[/̛ Ӵ   Q^^  !$ YV04ucvryomxB7z|eNPS+'y,! H B 5)//?!y|(v04!(/+9֯S΁eYBBè䉓1f8I D HXz HbQ)妧xV>f](Jp]S±-&$A33Vxel#&( `9Ȥ% 777Oy }se(@@f=t?TMC(B$F$CuU5LMca6XD6,+>ss_@umL qM0uÆ FQ]]I'cBK; "bd*w[ "#A?kɢeoj2F?[lV@ B0-rT:t:$B%h`ϵ67_}劋ٹ4͌Ϧ8-[ ˃y)NҫL`0'r2!4(B(Ix7qpACԢyxAh湮xΪ5+ѱl\B]Gqĉ/?]xq:L Сk*YD&5MbwϟcƠ::z?]~֍Wz^ KI|_L T0PUU!(t:y |g@1 (++C&UqhUS::u`ӦeDȴq,#^^C#Qq>s J{X>7ah V^wgmKu]tyo<-J.q]ѲL4nB:$I_?kǽ3(pASݫ,Xl1h'# 0 tk׭F6^QNᄒ`aR3,CcJ"X`Vċ Ӳ DIB._nX{'} B ux͗p羀h Z@6-R+Vyͷc2xݍW]V䥎 Hh$!egA}]=VY\.A pcs4]ǦM144BA$Pdy{K_=\kp(kaRF.=ɡi (1O aXdrP 4QG˰c-֯] ! A}i_׼kk.+(;,H$e+_G1}6888pUI<Ϗ+u7A zY ^rFe"Q(xy C3csa GنĠkVa릵 :k[dp@0p$@ ° }C7a:vuCovD咠( [vbGM|WG1O@:9L>=q @Ea .L,wvD.Pg=*.UU]YD"W`/#=f@u0@\ bUja`u@~l 0zlvlE<M{3k9+rP]]p 4̲<(-Y<#<,=ҁQfUAoOO)@i0n:|.*Ma۶Ntn]D7o y!6\BTmq\mnXرm3Ƕ ~4ҩ!| аn݆SL4e˗7LcrUe6`#dYigl]F__z1~\mrd30tCМt~8q"#I0"WEq+kjCӴH&j攼** uwy (קˑe >Tr{wiYq4a˲#Τ<XʼU#>F&)AD\^ukv,X P$L1Gߎ@D7giO3$fj^$7{Ao ݳN魮y8/p8lzx0E-Eh"Wm8s0MsɧJahewXSsO 4 ,d0Bzy"iCS 1 <e4uXq,9eNP5EBX*0B%ޣ«sEKð.M3,Fae 2e]q]&$A(؎4 |4SMeyUE4[iB(uJG cN8,'`sD}m&Ojkv_DMHnCAēN~7.< k7B2/Z?^9uV5weƼbJC!(RRfӅp( ghC$EV&޻pǥ+FoW'zm.N;|vR>p8OνG?rjhtEGc ]? ^cPCCCp <)1*P( x}}PTD"}_||868.$r>H 9Qz1:cՍېI%0ؿ(8AYY\l]dWӟ;oZ1aG}gEuL>{r/Nr޹X|YCCu]kv+s-˂i & ULò,T'JCA@Szz IL&|C=O6:KpQmHt[[7i{(C8E(Sl`8e!94MS1˧>4ff{kӴv IDAT}G1A_dC'9)>iYV,+ƓO<S rzB{y.a^?)/Ǯ9X2̂8R䩲W Iz]G^c яl6 ˴MeuMM :̼"8S\D,5譗/w㈕U!fҨk =1PH0pH`( ͙4o>Ļ}/+򍺮pI&Ŝ9OAKJ/^q}0@8w׿PF#}(;6F8]eAUXA@Q, ` 9 MզH\yJ4|OWTP(,z5p|5c HY4nlX;Ǯu,)O e`zqX @=] EƷ"B TU &Scwu\x׾U_9*_[*N8? C2!ھ};}P JaB[JZNf5MiwkQlY FҘ@74j,*b9,0g&ߘq=Y D?I Jy]KjլY/+&n0L6+04zw!+G*5]6 O@$ð^D4]#CGvg|K{xO~8Quo7ބeZQ!8 -w>?<d|.e+1nl3m5:a 46?Ͽ{+OfhA"9UU"łeM̀eY؎!8>N1b`@3 V0 YU,l a\eSOW5M+ 12Uy ebES8xjN!!|W|Mo03 ALz$\jGǑǝgB4V?ôrU' ªqc^ Gwϝ.HTc^S1IaZ;/oNd5B}XYێ]n MkU; JUuHB0RBs%,ʫGoB Uض#T㨊}t_<.vAQߚ#.va2LsgYC`Dxt`)}b5/  b@Q(qEP*t]E6Eee%G5b X%h po"A$k/Xx RI[n}G͸M@zq\@A*@ DID.fYds9,4ai'^Md<^h,p8\2@Q`!O\K%o͢{)FrxU#x6mMJoǫDMӎw\]<;R A e38ҫ.u(lSTׯK$ 4l&ʊ 4i@"*0fAe(dIp$]Ϧ/ˮO #㺫o< {~S*<[oi6u]q{KjI ǻ@?m{| f1~r&΀zedtڎLuc%/Mӵ UuP ]öNxygxS\]np/iph+~ɶ/1E Ǽf: ʴk\Xm6S5QUdc\hniC׮׀CA3+ n}myۦQKZWNe4/#:~|3:npm(g3phtMA>d_mNjinP??iTl:$ID&Ei쇦uY!QH%# s/14FS&5_?@є }4MKxйmgY^t9@lKйi ]Э}g_wwhnX۶0 @(+WU>SxaN a>rԖ=[!~qCQ‘X?kZfIENDB`webfakes/inst/examples/httpbin/data/0000755000176200001440000000000014172041777017227 5ustar liggesuserswebfakes/inst/examples/httpbin/data/example.json0000644000176200001440000000062714172041777021562 0ustar liggesusers{ "firstName": "John", "lastName": "Smith", "isAlive": true, "age": 27, "address": { "streetAddress": "21 2nd Street", "city": "New York", "state": "NY", "postalCode": "10021-3100" }, "phoneNumbers": [ { "type": "home", "number": "212 555-1234" }, { "type": "office", "number": "646 555-4567" } ], "children": [], "spouse": null } webfakes/inst/examples/httpbin/data/robots.txt0000644000176200001440000000003614172041777021277 0ustar liggesusersUser-agent: * Disallow: /deny webfakes/inst/examples/httpbin/data/utf8.html0000644000176200001440000003371714172041777021016 0ustar liggesusers

Unicode Demo

Taken from http://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-demo.txt

UTF-8 encoded sample plain-text file
‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾

Markus Kuhn [ˈmaʳkʊs kuːn]  — 2002-07-25 CC BY


The ASCII compatible UTF-8 encoding used in this plain-text file
is defined in Unicode, ISO 10646-1, and RFC 2279.


Using Unicode/UTF-8, you can write in emails and source code things such as

Mathematics and sciences:

  ∮ E⋅da = Q,  n → ∞, ∑ f(i) = ∏ g(i),      ⎧⎡⎛┌─────┐⎞⎤⎫
                                            ⎪⎢⎜│a²+b³ ⎟⎥⎪
  ∀x∈ℝ: ⌈x⌉ = −⌊−x⌋, α ∧ ¬β = ¬(¬α ∨ β),    ⎪⎢⎜│───── ⎟⎥⎪
                                            ⎪⎢⎜⎷ c₈   ⎟⎥⎪
  ℕ ⊆ ℕ₀ ⊂ ℤ ⊂ ℚ ⊂ ℝ ⊂ ℂ,                   ⎨⎢⎜       ⎟⎥⎬
                                            ⎪⎢⎜ ∞     ⎟⎥⎪
  ⊥ < a ≠ b ≡ c ≤ d ≪ ⊤ ⇒ (⟦A⟧ ⇔ ⟪B⟫),      ⎪⎢⎜ ⎲     ⎟⎥⎪
                                            ⎪⎢⎜ ⎳aⁱ-bⁱ⎟⎥⎪
  2H₂ + O₂ ⇌ 2H₂O, R = 4.7 kΩ, ⌀ 200 mm     ⎩⎣⎝i=1    ⎠⎦⎭

Linguistics and dictionaries:

  ði ıntəˈnæʃənəl fəˈnɛtık əsoʊsiˈeıʃn
  Y [ˈʏpsilɔn], Yen [jɛn], Yoga [ˈjoːgɑ]

APL:

  ((V⍳V)=⍳⍴V)/V←,V    ⌷←⍳→⍴∆∇⊃‾⍎⍕⌈

Nicer typography in plain text files:

  ╔══════════════════════════════════════════╗
  ║                                          ║
  ║   • ‘single’ and “double” quotes         ║
  ║                                          ║
  ║   • Curly apostrophes: “We’ve been here” ║
  ║                                          ║
  ║   • Latin-1 apostrophe and accents: '´`  ║
  ║                                          ║
  ║   • ‚deutsche‘ „Anführungszeichen“       ║
  ║                                          ║
  ║   • †, ‡, ‰, •, 3–4, —, −5/+5, ™, …      ║
  ║                                          ║
  ║   • ASCII safety test: 1lI|, 0OD, 8B     ║
  ║                      ╭─────────╮         ║
  ║   • the euro symbol: │ 14.95 € │         ║
  ║                      ╰─────────╯         ║
  ╚══════════════════════════════════════════╝

Combining characters:

  STARGΛ̊TE SG-1, a = v̇ = r̈, a⃑ ⊥ b⃑

Greek (in Polytonic):

  The Greek anthem:

  Σὲ γνωρίζω ἀπὸ τὴν κόψη
  τοῦ σπαθιοῦ τὴν τρομερή,
  σὲ γνωρίζω ἀπὸ τὴν ὄψη
  ποὺ μὲ βία μετράει τὴ γῆ.

  ᾿Απ᾿ τὰ κόκκαλα βγαλμένη
  τῶν ῾Ελλήνων τὰ ἱερά
  καὶ σὰν πρῶτα ἀνδρειωμένη
  χαῖρε, ὦ χαῖρε, ᾿Ελευθεριά!

  From a speech of Demosthenes in the 4th century BC:

  Οὐχὶ ταὐτὰ παρίσταταί μοι γιγνώσκειν, ὦ ἄνδρες ᾿Αθηναῖοι,
  ὅταν τ᾿ εἰς τὰ πράγματα ἀποβλέψω καὶ ὅταν πρὸς τοὺς
  λόγους οὓς ἀκούω· τοὺς μὲν γὰρ λόγους περὶ τοῦ
  τιμωρήσασθαι Φίλιππον ὁρῶ γιγνομένους, τὰ δὲ πράγματ᾿
  εἰς τοῦτο προήκοντα,  ὥσθ᾿ ὅπως μὴ πεισόμεθ᾿ αὐτοὶ
  πρότερον κακῶς σκέψασθαι δέον. οὐδέν οὖν ἄλλο μοι δοκοῦσιν
  οἱ τὰ τοιαῦτα λέγοντες ἢ τὴν ὑπόθεσιν, περὶ ἧς βουλεύεσθαι,
  οὐχὶ τὴν οὖσαν παριστάντες ὑμῖν ἁμαρτάνειν. ἐγὼ δέ, ὅτι μέν
  ποτ᾿ ἐξῆν τῇ πόλει καὶ τὰ αὑτῆς ἔχειν ἀσφαλῶς καὶ Φίλιππον
  τιμωρήσασθαι, καὶ μάλ᾿ ἀκριβῶς οἶδα· ἐπ᾿ ἐμοῦ γάρ, οὐ πάλαι
  γέγονεν ταῦτ᾿ ἀμφότερα· νῦν μέντοι πέπεισμαι τοῦθ᾿ ἱκανὸν
  προλαβεῖν ἡμῖν εἶναι τὴν πρώτην, ὅπως τοὺς συμμάχους
  σώσομεν. ἐὰν γὰρ τοῦτο βεβαίως ὑπάρξῃ, τότε καὶ περὶ τοῦ
  τίνα τιμωρήσεταί τις καὶ ὃν τρόπον ἐξέσται σκοπεῖν· πρὶν δὲ
  τὴν ἀρχὴν ὀρθῶς ὑποθέσθαι, μάταιον ἡγοῦμαι περὶ τῆς
  τελευτῆς ὁντινοῦν ποιεῖσθαι λόγον.

  Δημοσθένους, Γ´ ᾿Ολυνθιακὸς

Georgian:

  From a Unicode conference invitation:

  გთხოვთ ახლავე გაიაროთ რეგისტრაცია Unicode-ის მეათე საერთაშორისო
  კონფერენციაზე დასასწრებად, რომელიც გაიმართება 10-12 მარტს,
  ქ. მაინცში, გერმანიაში. კონფერენცია შეჰკრებს ერთად მსოფლიოს
  ექსპერტებს ისეთ დარგებში როგორიცაა ინტერნეტი და Unicode-ი,
  ინტერნაციონალიზაცია და ლოკალიზაცია, Unicode-ის გამოყენება
  ოპერაციულ სისტემებსა, და გამოყენებით პროგრამებში, შრიფტებში,
  ტექსტების დამუშავებასა და მრავალენოვან კომპიუტერულ სისტემებში.

Russian:

  From a Unicode conference invitation:

  Зарегистрируйтесь сейчас на Десятую Международную Конференцию по
  Unicode, которая состоится 10-12 марта 1997 года в Майнце в Германии.
  Конференция соберет широкий круг экспертов по  вопросам глобального
  Интернета и Unicode, локализации и интернационализации, воплощению и
  применению Unicode в различных операционных системах и программных
  приложениях, шрифтах, верстке и многоязычных компьютерных системах.

Thai (UCS Level 2):

  Excerpt from a poetry on The Romance of The Three Kingdoms (a Chinese
  classic 'San Gua'):

  [----------------------------|------------------------]
    ๏ แผ่นดินฮั่นเสื่อมโทรมแสนสังเวช  พระปกเกศกองบู๊กู้ขึ้นใหม่
  สิบสองกษัตริย์ก่อนหน้าแลถัดไป       สององค์ไซร้โง่เขลาเบาปัญญา
    ทรงนับถือขันทีเป็นที่พึ่ง           บ้านเมืองจึงวิปริตเป็นนักหนา
  โฮจิ๋นเรียกทัพทั่วหัวเมืองมา         หมายจะฆ่ามดชั่วตัวสำคัญ
    เหมือนขับไสไล่เสือจากเคหา      รับหมาป่าเข้ามาเลยอาสัญ
  ฝ่ายอ้องอุ้นยุแยกให้แตกกัน          ใช้สาวนั้นเป็นชนวนชื่นชวนใจ
    พลันลิฉุยกุยกีกลับก่อเหตุ          ช่างอาเพศจริงหนาฟ้าร้องไห้
  ต้องรบราฆ่าฟันจนบรรลัย           ฤๅหาใครค้ำชูกู้บรรลังก์ ฯ

  (The above is a two-column text. If combining characters are handled
  correctly, the lines of the second column should be aligned with the
  | character above.)

Ethiopian:

  Proverbs in the Amharic language:

  ሰማይ አይታረስ ንጉሥ አይከሰስ።
  ብላ ካለኝ እንደአባቴ በቆመጠኝ።
  ጌጥ ያለቤቱ ቁምጥና ነው።
  ደሀ በሕልሙ ቅቤ ባይጠጣ ንጣት በገደለው።
  የአፍ ወለምታ በቅቤ አይታሽም።
  አይጥ በበላ ዳዋ ተመታ።
  ሲተረጉሙ ይደረግሙ።
  ቀስ በቀስ፥ ዕንቁላል በእግሩ ይሄዳል።
  ድር ቢያብር አንበሳ ያስር።
  ሰው እንደቤቱ እንጅ እንደ ጉረቤቱ አይተዳደርም።
  እግዜር የከፈተውን ጉሮሮ ሳይዘጋው አይድርም።
  የጎረቤት ሌባ፥ ቢያዩት ይስቅ ባያዩት ያጠልቅ።
  ሥራ ከመፍታት ልጄን ላፋታት።
  ዓባይ ማደሪያ የለው፥ ግንድ ይዞ ይዞራል።
  የእስላም አገሩ መካ የአሞራ አገሩ ዋርካ።
  ተንጋሎ ቢተፉ ተመልሶ ባፉ።
  ወዳጅህ ማር ቢሆን ጨርስህ አትላሰው።
  እግርህን በፍራሽህ ልክ ዘርጋ።

Runes:

  ᚻᛖ ᚳᚹᚫᚦ ᚦᚫᛏ ᚻᛖ ᛒᚢᛞᛖ ᚩᚾ ᚦᚫᛗ ᛚᚪᚾᛞᛖ ᚾᚩᚱᚦᚹᛖᚪᚱᛞᚢᛗ ᚹᛁᚦ ᚦᚪ ᚹᛖᛥᚫ

  (Old English, which transcribed into Latin reads 'He cwaeth that he
  bude thaem lande northweardum with tha Westsae.' and means 'He said
  that he lived in the northern land near the Western Sea.')

Braille:

  ⡌⠁⠧⠑ ⠼⠁⠒  ⡍⠜⠇⠑⠹⠰⠎ ⡣⠕⠌

  ⡍⠜⠇⠑⠹ ⠺⠁⠎ ⠙⠑⠁⠙⠒ ⠞⠕ ⠃⠑⠛⠔ ⠺⠊⠹⠲ ⡹⠻⠑ ⠊⠎ ⠝⠕ ⠙⠳⠃⠞
  ⠱⠁⠞⠑⠧⠻ ⠁⠃⠳⠞ ⠹⠁⠞⠲ ⡹⠑ ⠗⠑⠛⠊⠌⠻ ⠕⠋ ⠙⠊⠎ ⠃⠥⠗⠊⠁⠇ ⠺⠁⠎
  ⠎⠊⠛⠝⠫ ⠃⠹ ⠹⠑ ⠊⠇⠻⠛⠹⠍⠁⠝⠂ ⠹⠑ ⠊⠇⠻⠅⠂ ⠹⠑ ⠥⠝⠙⠻⠞⠁⠅⠻⠂
  ⠁⠝⠙ ⠹⠑ ⠡⠊⠑⠋ ⠍⠳⠗⠝⠻⠲ ⡎⠊⠗⠕⠕⠛⠑ ⠎⠊⠛⠝⠫ ⠊⠞⠲ ⡁⠝⠙
  ⡎⠊⠗⠕⠕⠛⠑⠰⠎ ⠝⠁⠍⠑ ⠺⠁⠎ ⠛⠕⠕⠙ ⠥⠏⠕⠝ ⠰⡡⠁⠝⠛⠑⠂ ⠋⠕⠗ ⠁⠝⠹⠹⠔⠛ ⠙⠑
  ⠡⠕⠎⠑ ⠞⠕ ⠏⠥⠞ ⠙⠊⠎ ⠙⠁⠝⠙ ⠞⠕⠲

  ⡕⠇⠙ ⡍⠜⠇⠑⠹ ⠺⠁⠎ ⠁⠎ ⠙⠑⠁⠙ ⠁⠎ ⠁ ⠙⠕⠕⠗⠤⠝⠁⠊⠇⠲

  ⡍⠔⠙⠖ ⡊ ⠙⠕⠝⠰⠞ ⠍⠑⠁⠝ ⠞⠕ ⠎⠁⠹ ⠹⠁⠞ ⡊ ⠅⠝⠪⠂ ⠕⠋ ⠍⠹
  ⠪⠝ ⠅⠝⠪⠇⠫⠛⠑⠂ ⠱⠁⠞ ⠹⠻⠑ ⠊⠎ ⠏⠜⠞⠊⠊⠥⠇⠜⠇⠹ ⠙⠑⠁⠙ ⠁⠃⠳⠞
  ⠁ ⠙⠕⠕⠗⠤⠝⠁⠊⠇⠲ ⡊ ⠍⠊⠣⠞ ⠙⠁⠧⠑ ⠃⠑⠲ ⠔⠊⠇⠔⠫⠂ ⠍⠹⠎⠑⠇⠋⠂ ⠞⠕
  ⠗⠑⠛⠜⠙ ⠁ ⠊⠕⠋⠋⠔⠤⠝⠁⠊⠇ ⠁⠎ ⠹⠑ ⠙⠑⠁⠙⠑⠌ ⠏⠊⠑⠊⠑ ⠕⠋ ⠊⠗⠕⠝⠍⠕⠝⠛⠻⠹
  ⠔ ⠹⠑ ⠞⠗⠁⠙⠑⠲ ⡃⠥⠞ ⠹⠑ ⠺⠊⠎⠙⠕⠍ ⠕⠋ ⠳⠗ ⠁⠝⠊⠑⠌⠕⠗⠎
  ⠊⠎ ⠔ ⠹⠑ ⠎⠊⠍⠊⠇⠑⠆ ⠁⠝⠙ ⠍⠹ ⠥⠝⠙⠁⠇⠇⠪⠫ ⠙⠁⠝⠙⠎
  ⠩⠁⠇⠇ ⠝⠕⠞ ⠙⠊⠌⠥⠗⠃ ⠊⠞⠂ ⠕⠗ ⠹⠑ ⡊⠳⠝⠞⠗⠹⠰⠎ ⠙⠕⠝⠑ ⠋⠕⠗⠲ ⡹⠳
  ⠺⠊⠇⠇ ⠹⠻⠑⠋⠕⠗⠑ ⠏⠻⠍⠊⠞ ⠍⠑ ⠞⠕ ⠗⠑⠏⠑⠁⠞⠂ ⠑⠍⠏⠙⠁⠞⠊⠊⠁⠇⠇⠹⠂ ⠹⠁⠞
  ⡍⠜⠇⠑⠹ ⠺⠁⠎ ⠁⠎ ⠙⠑⠁⠙ ⠁⠎ ⠁ ⠙⠕⠕⠗⠤⠝⠁⠊⠇⠲

  (The first couple of paragraphs of "A Christmas Carol" by Dickens)

Compact font selection example text:

  ABCDEFGHIJKLMNOPQRSTUVWXYZ /0123456789
  abcdefghijklmnopqrstuvwxyz £©µÀÆÖÞßéöÿ
  –—‘“”„†•…‰™œŠŸž€ ΑΒΓΔΩαβγδω АБВГДабвгд
  ∀∂∈ℝ∧∪≡∞ ↑↗↨↻⇣ ┐┼╔╘░►☺♀ fi�⑀₂ἠḂӥẄɐː⍎אԱა

Greetings in various languages:

  Hello world, Καλημέρα κόσμε, コンニチハ

Box drawing alignment tests:                                          █
                                                                      ▉
  ╔══╦══╗  ┌──┬──┐  ╭──┬──╮  ╭──┬──╮  ┏━━┳━━┓  ┎┒┏┑   ╷  ╻ ┏┯┓ ┌┰┐    ▊ ╱╲╱╲╳╳╳
  ║┌─╨─┐║  │╔═╧═╗│  │╒═╪═╕│  │╓─╁─╖│  ┃┌─╂─┐┃  ┗╃╄┙  ╶┼╴╺╋╸┠┼┨ ┝╋┥    ▋ ╲╱╲╱╳╳╳
  ║│╲ ╱│║  │║   ║│  ││ │ ││  │║ ┃ ║│  ┃│ ╿ │┃  ┍╅╆┓   ╵  ╹ ┗┷┛ └┸┘    ▌ ╱╲╱╲╳╳╳
  ╠╡ ╳ ╞╣  ├╢   ╟┤  ├┼─┼─┼┤  ├╫─╂─╫┤  ┣┿╾┼╼┿┫  ┕┛┖┚     ┌┄┄┐ ╎ ┏┅┅┓ ┋ ▍ ╲╱╲╱╳╳╳
  ║│╱ ╲│║  │║   ║│  ││ │ ││  │║ ┃ ║│  ┃│ ╽ │┃  ░░▒▒▓▓██ ┊  ┆ ╎ ╏  ┇ ┋ ▎
  ║└─╥─┘║  │╚═╤═╝│  │╘═╪═╛│  │╙─╀─╜│  ┃└─╂─┘┃  ░░▒▒▓▓██ ┊  ┆ ╎ ╏  ┇ ┋ ▏
  ╚══╩══╝  └──┴──┘  ╰──┴──╯  ╰──┴──╯  ┗━━┻━━┛  ▗▄▖▛▀▜   └╌╌┘ ╎ ┗╍╍┛ ┋  ▁▂▃▄▅▆▇█
                                               ▝▀▘▙▄▟
webfakes/inst/examples/httpbin/data/example.html0000644000176200001440000000264314172041777021555 0ustar liggesusers

Don Quixote by Miguel de Cervantes

In a village of La Mancha, the name of which I have no desire to call to mind, there lived not long since one of those gentlemen that keep a lance in the lance-rack, an old buckler, a lean hack, and a greyhound for coursing. An olla of rather more beef than mutton, a salad on most nights, scraps on Saturdays, lentils on Fridays, and a pigeon or so extra on Sundays, made away with three-quarters of his income. The rest of it went in a doublet of fine cloth and velvet breeches and shoes to match for holidays, while on week-days he made a brave figure in his best homespun. He had in his house a housekeeper past forty, a niece under twenty, and a lad for the field and market-place, who used to saddle the hack as well as handle the bill-hook. The age of this gentleman of ours was bordering on fifty; he was of a hardy habit, spare, gaunt-featured, a very early riser and a great sportsman. They will have it his surname was Quixada or Quesada (for here there is some difference of opinion among the authors who write on the subject), although from reasonable conjectures it seems plain that he was called Quexana. This, however, is of but little importance to our tale; it will be enough not to stray a hair’s breadth from the truth in the telling of it.

webfakes/inst/examples/httpbin/data/example.xml0000644000176200001440000000114214172041777021402 0ustar liggesusers
New York 10021-3100 NY 21 2nd Street
27 John true Smith 212 555-1234 home 646 555-4567 office
webfakes/inst/examples/httpbin/data/deny.txt0000644000176200001440000000042714172041777020732 0ustar liggesusers -------------- Get out of here -------------- \ \ \ |\___/| ==) ^Y^ (== \ ^ / )=*=( / \ | | /| | | |\ \| | |_|/\ jgs //_// ___/ \_) webfakes/inst/examples/httpbin/doc-template.hbs0000644000176200001440000000072114172041777021372 0ustar liggesusers {{title}} {{{redocHead}}} {{{redocHTML}}} webfakes/inst/examples/httpbin/app.R0000644000176200001440000000014414172041777017220 0ustar liggesusers library(webfakes) app <- httpbin_app() app$listen(as.integer(Sys.getenv("PORT", NA_character_))) webfakes/inst/examples/static/0000755000176200001440000000000014172041777016135 5ustar liggesuserswebfakes/inst/examples/static/public/0000755000176200001440000000000014172041777017413 5ustar liggesuserswebfakes/inst/examples/static/public/bar/0000755000176200001440000000000014172041777020157 5ustar liggesuserswebfakes/inst/examples/static/public/bar/foo.txt0000644000176200001440000000002514172041777021500 0ustar liggesusersThis is a text file. webfakes/inst/examples/static/public/foo/0000755000176200001440000000000014172041777020176 5ustar liggesuserswebfakes/inst/examples/static/public/foo/bar.json0000644000176200001440000000005314172041777021633 0ustar liggesusers{ "foo": "bar", "bar": [1, 2, 3] } webfakes/inst/examples/static/public/foo/bar.html0000644000176200001440000000006414172041777021630 0ustar liggesusersHello world! webfakes/inst/examples/static/app.R0000644000176200001440000000022014172041777017032 0ustar liggesusers library(webfakes) app <- new_app() app$use(mw_log()) app$use(mw_static("public")) app$listen(as.integer(Sys.getenv("PORT", NA_character_))) webfakes/inst/examples/send-file/0000755000176200001440000000000014172041777016514 5ustar liggesuserswebfakes/inst/examples/send-file/Rlogo.png0000644000176200001440000002700614172041777020311 0ustar liggesusersPNG  IHDRdLbKGD pHYs.#.#x?vtIME' IDATx}w]e]O/[2d24H.(b(XAI\Z(CPB ϤM?3sgϜ$`;s2^]Yk(|S72JӴ*˲*ljv[i&l;6=aALv2EQIx1̓'Eԧff=rc>f$q&ڎ3u&۱Cu][wA4Lض u\`;&D@uiz3òCӾm'Nvwߤ6n.˅>Ѷcm۪v](7s[u] LeY( .ׅ8u8q\0YBQKh~76a9l?w_m+eEQE{}/a`[vݒzx[3uJ8 .na4EU5/)1ʋԮ]Ǫ#˴۶8(PěOnߍz,,ׅe T{-uJR$ǁm۰ٶ ۶`6,˂8@,|GnzŸLؓ2-Ӻ6 &P #`CQ޾#SUw/FTmۀ¶P8c_p]-SaIxb[lˆi 1 òUU^u ?tOj3M|۱#K( mwB"4M@ tdxRy3#aۻm8Fm{@X6,˄eZ0LeT 3oO% ?`LK øuoɞo> !0MA5 u=io?Р `g^R`;,˂i0-eum Ʋ,X 4a0=Z]U} T_bq8exJH˱x4!(2r  i.@348za0,X]*JD}Ӵ&ζ풭2*!B@34òi2M4Ų-4at]aPpU_y{VT˶INPe!Mҩ$t]E( ='x8EQ ,MӠit]aucXnS2Tq]qnS Àxa8.4Mi0b& ]aui&tW89;XpXy'L&}nSOQP8P,"N\dYqX  " y.`:TM(exb0-=h[k ^P8woE5EO=v,(ݿVg eq<\ǁ( e0m-ӄ=P)yYcE~ֶ鄐7D׃<Es8yP(/@(CQdȊ IQeo4GT ǶKIiP(B,-'CA Àa M8A4 MSjZ~tMנh}羻fCin̓ !8CYY2`5f @<G<GmM*+*P0%Q 5 #ni]Ne['s LB|}%j8}y`px׮'ˊq:N˱8E狎 # KP,C4 / 0E@4M#bb$3iE:/Cń6TW,d (EA.];cR)Bt+?c^[|JƵ7\vQضMqx<{{u\ **$Q(B\p/I_t? ;=ErLt:mUU 0i,Z(2$YBP%]]y&$i!kllgS>.ҲsL8 ˀJD$B.ȐUPͻCY4U] ?FUE>,[/̛ Ӵ   Q^^  !$ YV04ucvryomxB7z|eNPS+'y,! H B 5)//?!y|(v04!(/+9֯S΁eYBBè䉓1f8I D HXz HbQ)妧xV>f](Jp]S±-&$A33Vxel#&( `9Ȥ% 777Oy }se(@@f=t?TMC(B$F$CuU5LMca6XD6,+>ss_@umL qM0uÆ FQ]]I'cBK; "bd*w[ "#A?kɢeoj2F?[lV@ B0-rT:t:$B%h`ϵ67_}劋ٹ4͌Ϧ8-[ ˃y)NҫL`0'r2!4(B(Ix7qpACԢyxAh湮xΪ5+ѱl\B]Gqĉ/?]xq:L Сk*YD&5MbwϟcƠ::z?]~֍Wz^ KI|_L T0PUU!(t:y |g@1 (++C&UqhUS::u`ӦeDȴq,#^^C#Qq>s J{X>7ah V^wgmKu]tyo<-J.q]ѲL4nB:$I_?kǽ3(pASݫ,Xl1h'# 0 tk׭F6^QNᄒ`aR3,CcJ"X`Vċ Ӳ DIB._nX{'} B ux͗p羀h Z@6-R+Vyͷc2xݍW]V䥎 Hh$!egA}]=VY\.A pcs4]ǦM144BA$Pdy{K_=\kp(kaRF.=ɡi (1O aXdrP 4QG˰c-֯] ! A}i_׼kk.+(;,H$e+_G1}6888pUI<Ϗ+u7A zY ^rFe"Q(xy C3csa GنĠkVa릵 :k[dp@0p$@ ° }C7a:vuCovD咠( [vbGM|WG1O@:9L>=q @Ea .L,wvD.Pg=*.UU]YD"W`/#=f@u0@\ bUja`u@~l 0zlvlE<M{3k9+rP]]p 4̲<(-Y<#<,=ҁQfUAoOO)@i0n:|.*Ma۶Ntn]D7o y!6\BTmq\mnXرm3Ƕ ~4ҩ!| аn݆SL4e˗7LcrUe6`#dYigl]F__z1~\mrd30tCМt~8q"#I0"WEq+kjCӴH&j攼** uwy (קˑe >Tr{wiYq4a˲#Τ<XʼU#>F&)AD\^ukv,X P$L1Gߎ@D7giO3$fj^$7{Ao ݳN魮y8/p8lzx0E-Eh"Wm8s0MsɧJahewXSsO 4 ,d0Bzy"iCS 1 <e4uXq,9eNP5EBX*0B%ޣ«sEKð.M3,Fae 2e]q]&$A(؎4 |4SMeyUE4[iB(uJG cN8,'`sD}m&Ojkv_DMHnCAēN~7.< k7B2/Z?^9uV5weƼbJC!(RRfӅp( ghC$EV&޻pǥ+FoW'zm.N;|vR>p8OνG?rjhtEGc ]? ^cPCCCp <)1*P( x}}PTD"}_||868.$r>H 9Qz1:cՍېI%0ؿ(8AYY\l]dWӟ;oZ1aG}gEuL>{r/Nr޹X|YCCu]kv+s-˂i & ULò,T'JCA@Szz IL&|C=O6:KpQmHt[[7i{(C8E(Sl`8e!94MS1˧>4ff{kӴv IDAT}G1A_dC'9)>iYV,+ƓO<S rzB{y.a^?)/Ǯ9X2̂8R䩲W Iz]G^c яl6 ˴MeuMM :̼"8S\D,5譗/w㈕U!fҨk =1PH0pH`( ͙4o>Ļ}/+򍺮pI&Ŝ9OAKJ/^q}0@8w׿PF#}(;6F8]eAUXA@Q, ` 9 MզH\yJ4|OWTP(,z5p|5c HY4nlX;Ǯu,)O e`zqX @=] EƷ"B TU &Scwu\x׾U_9*_[*N8? C2!ھ};}P JaB[JZNf5MiwkQlY FҘ@74j,*b9,0g&ߘq=Y D?I Jy]KjլY/+&n0L6+04zw!+G*5]6 O@$ð^D4]#CGvg|K{xO~8Quo7ބeZQ!8 -w>?<d|.e+1nl3m5:a 46?Ͽ{+OfhA"9UU"łeM̀eY؎!8>N1b`@3 V0 YU,l a\eSOW5M+ 12Uy ebES8xjN!!|W|Mo03 ALz$\jGǑǝgB4V?ôrU' ªqc^ Gwϝ.HTc^S1IaZ;/oNd5B}XYێ]n MkU; JUuHB0RBs%,ʫGoB Uض#T㨊}t_<.vAQߚ#.va2LsgYC`Dxt`)}b5/  b@Q(qEP*t]E6Eee%G5b X%h po"A$k/Xx RI[n}G͸M@zq\@A*@ DID.fYds9,4ai'^Md<^h,p8\2@Q`!O\K%o͢{)FrxU#x6mMJoǫDMӎw\]<;R A e38ҫ.u(lSTׯK$ 4l&ʊ 4i@"*0fAe(dIp$]Ϧ/ˮO #㺫o< {~S*<[oi6u]q{KjI ǻ@?m{| f1~r&΀zedtڎLuc%/Mӵ UuP ]öNxygxS\]np/iph+~ɶ/1E Ǽf: ʴk\Xm6S5QUdc\hniC׮׀CA3+ n}myۦQKZWNe4/#:~|3:npm(g3phtMA>d_mNjinP??iTl:$ID&Ei쇦uY!QH%# s/14FS&5_?@є }4MKxйmgY^t9@lKйi ]Э}g_wwhnX۶0 @(+WU>SxaN a>rԖ=[!~qCQ‘X?kZfIENDB`webfakes/inst/examples/send-file/app.R0000644000176200001440000000024414172041777017417 0ustar liggesusers library(webfakes) app <- new_app() app$use(mw_log()) app$use(mw_etag()) app$get("/logo", function(req, res) { res$send_file("Rlogo.png") }) app$listen(3000L) webfakes/README.md0000644000176200001440000001107714740243712013332 0ustar liggesusers # webfakes > Your own web server for happy HTTP testing [![R build status](https://github.com/r-lib/webfakes/workflows/R-CMD-check/badge.svg)](https://github.com/r-lib/webfakes/actions) [![CRAN status](https://www.r-pkg.org/badges/version/webfakes)](https://CRAN.R-project.org/package=webfakes) [![R-CMD-check](https://github.com/r-lib/webfakes/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/r-lib/webfakes/actions/workflows/R-CMD-check.yaml) [![Codecov test coverage](https://codecov.io/gh/r-lib/webfakes/graph/badge.svg)](https://app.codecov.io/gh/r-lib/webfakes) Lightweight fake web apps for testing. Built using the [civetweb](https://github.com/civetweb/civetweb) embedded web server. ## Features - Complete web app framework, define handlers for HTTP requests in R. - Write your own app for your custom test cases; our use app similar to the `https://httpbin.org` API, so often you don’t need to write your own web app (e.g. if you are writing an HTTP client (httr, curl, crul). - Run one web app per test suite, per test file or per test case. - Flexible path matching, with parameters and regular expressions. - Built in templating system using glue or bring your own template engine. - Middleware to parse JSON, multipart and URL encoded request bodies. - A web app is just an R object. It can be saved to disk, copied to another R process, etc. - A web app is extensible, by adding new routes and middleware to it. - Helper functions for sending JSON, files from disk, etc. - App-specific environment to store any data including data from requests to the fake app. - After a web app is launched from R, you can interact with it from R but also from the command line, your browser, etc. Nice for debugging. - The web server runs in the R process, so it has no problems with local firewalls. - Multi-threaded web server supports concurrent HTTP requests. - Limit download speed to simulate low bandwidth. ## Optional dependencies - The jsonlite package is needed for the `mw_json()` middleware, the `response$send_json()` method and the `httpbin_app()` app. - The glue package is needed for the `tmpl_glue()` template engine. - The callr package is needed for `new_app_process()` and `local_app_process` to work. - The `/brotli` endpoint of `httpbin_app()` needs the brotli package. - The `/deflate` endpoint of `httpbin_app()` needs the zip package. - The `/digest-auth` endpoint of `httpbin_app()` needs the digest package. - `git_app()` requires the processx package. ## Installation Install the release version from CRAN: ``` r install.packages("webfakes") ``` If you need the development version of the package, install it from GitHub: ``` r pak::pak("r-lib/webfakes") ``` ## Usage Start a web app at the beginning of your tests or test file, and stop it after. Here is an example with the testthat package. Suppose you want to test that your `get_hello()` function can query an API: `local_app_process()` helps you clean up the web server process after the test block, or test file. It is similar to the `withr::local_*` functions. ``` r app <- webfakes::new_app() app$get("/hello/:user", function(req, res) { res$send(paste0("Hello ", req$params$user, "!")) }) web <- webfakes::local_app_process(app) test_that("can use hello API", { url <- web$url("/hello/Gabor") expect_equal(get_hello(url), "Hello Gabor!") }) ``` When testing HTTP clients you can often use the built in `httpbin_app()`: ``` r httpbin <- webfakes::local_app_process(webfakes::httpbin_app()) ``` ``` r test_that("HTTP errors are caught", { url <- httpbin$url("/status/404") resp <- httr::GET(url) expect_error(httr::stop_for_status(resp), class = "http_404") }) ``` #> Test passed 😸 ## Documentation See ## Links ### Other solutions for HTTP testing in R: - [vcr](https://github.com/ropensci/vcr) - [httptest](https://github.com/nealrichardson/httptest) ### R web application frameworks webfakes focuses on testing, these packages are for writing real web apps: - [shiny](https://github.com/rstudio/shiny) - [opencpu](https://www.opencpu.org/) - [plumber](https://github.com/rstudio/plumber) - [fiery](https://github.com/thomasp85/fiery) - [RestRserve](https://github.com/rexyai/RestRserve) ## Code of Conduct Please note that the webfakes project is released with a [Contributor Code of Conduct](https://webfakes.r-lib.org/dev/CODE_OF_CONDUCT.html). By contributing to this project, you agree to abide by its terms. ## License MIT © RStudio webfakes/man/0000755000176200001440000000000014740243712012620 5ustar liggesuserswebfakes/man/local_app_process.Rd0000644000176200001440000000144314172041777016607 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/local-app-process.R \name{local_app_process} \alias{local_app_process} \title{App process that is cleaned up automatically} \usage{ local_app_process(app, ..., .local_envir = parent.frame()) } \arguments{ \item{app}{\code{webfakes_app} object, the web app to run.} \item{...}{Passed to \code{\link[=new_app_process]{new_app_process()}}.} \item{.local_envir}{The environment to attach the process cleanup to. Typically a frame. When this frame finishes, the process is stopped.} } \description{ You can start the process with an explicit \verb{$start()} call. Alternatively it starts up at the first \verb{$url()} or \verb{$get_port()} call. } \seealso{ \code{\link[=new_app_process]{new_app_process()}} for more details. } webfakes/man/rmd-fragments/0000755000176200001440000000000014172041777015374 5ustar liggesuserswebfakes/man/rmd-fragments/oauth2.Rmd0000644000176200001440000000120714172041777017242 0ustar liggesusersThe webfakes package comes with two fake apps that allow to imitate the OAuth2.0 flow in your test cases. (See [Aaron Parecki's tutorial](https://aaronparecki.com/oauth-2-simplified/) for a good introduction to OAuth2.0.) One app (`oauth2_resource_app()`) is the API server that serves both as the resource and provides authorization. `oauth2_third_party_app()` plays the role of the third-party app. They are useful when testing or demonstrating code handling OAuth2.0 authorization, token caching, etc. in a package. The apps can be used in your tests directly, or you could adapt one or both of them to better mimic a particular OAuth2.0 flow. webfakes/man/glossary.Rd0000644000176200001440000001057614740243712014763 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/docs.R \name{glossary} \alias{glossary} \title{webfakes glossary} \description{ webfakes glossary } \section{Webfakes glossary}{ The webfakes package uses vocabulary that is standard for web apps, especially those developed with Express.js, but not necessarily well known to all R package developers. \subsection{app}{ (Also: fake web app, webfakes app.) A web application that can be served by webfakes's web server, typically in another process, an \emph{app process}. Sometimes we call it a \emph{fake} web app, to emphasize that we use it for testing real web apps and APIs. You can create a webfakes app with the \code{new_app()} function. A webfakes app is an R object that you can save to disk with \code{saveRDS()} , and you can also include it in your package. You can start an with its \verb{$listen()} method. Since the main R process runs that test suite code, you usually run them in a subprocess, see \code{new_app_process()} or \code{local_app_process()}. } \subsection{app process}{ (Also: web server process, webfakes subprocess.) An app process is an R subprocess, started from the main R process, to serve a webfakes \emph{app}. You can create an app process object with \code{new_app_process()} or \code{local_app_process()}. By default the actual process does not start yet, when you create it. You can start it explicitly with the \verb{$start} method of the app process object, or by querying its URL with \verb{$url()} or its port with \verb{$get_port()}. For test cases, you typically start app processes at these places: \itemize{ \item In a \code{setup*.R} file, to start an app that the whole test suite can use. \item Alternatively, in a \code{helper*.R} file, to start an app that the whole test suite can use, and it works better for interactive development. \item At the beginning of a test file, to create an app for a single test file. \item Inside \code{test_that()}, to create an app for a single test block. } See the How-to for details about each. } \subsection{handler}{ (Or handler function.) A handler is a \emph{route} or a \emph{middleware}. } \subsection{handler stack}{ This is a stack of handler functions, which are called by the app one after the other, passing the request and response objects to them. Handlers typically manipulate the request and/or response objects. A terminal handler instructs the app to return the response to the HTTP client. A non-terminal handler tells the app to keep calling handlers, by returning the string \code{"next"}. } \subsection{httpbin app}{ This is an example app, which implements the excellent \verb{https://httpbin.org/} web service. You can use it to simulate certain HTTP responses. It is most handy for HTTP clients, but potentially useful for other tools as well. Use \code{httpbin_app()} to create an instance of this app. } \subsection{middleware}{ A middleware is a handler function that is not bound to a path. It is called by the router, like other handler functions. It may manipulate the request or the response, or can have a side effect. Some example built-in middleware functions in webfakes: \itemize{ \item \code{mw_json()} parses a request's JSON body into an R object. \item \code{mw_log()} logs requests and responses to the screen or to a file. \item \code{mw_static()} serves static files from the directory. } You can also write your own middleware functions. } \subsection{path matching}{ The router performs path matching when it goes over the handler stack. If the HTTP method and path of a \emph{route} match the HTTP method and URL of the request, then the handler is called, otherwise it is not. Paths can have parameters and be regular expressions. See \code{?new_regexp()} for regular expressions and "Path parameters" in \code{?new_app()} for parameters. } \subsection{route}{ A route is a handler function that is bound to certain paths of you web app. If the request URL matches the path of the route, then the handler function is called, to give it a chance to send the appropriate response. Route paths may have parameters or they can be regular expressions in webfakes. } \subsection{routing}{ Routing is the process of going over the handlers stack, and calling handler functions, one after the other, until one handles the request. If a handler function is a \emph{route}, then the router only calls it if its path matches the request URL. } } webfakes/man/new_regexp.Rd0000644000176200001440000000140114172041777015254 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/path.R \name{new_regexp} \alias{new_regexp} \alias{webfakes_regexp} \title{Create a new regular expression to use in webfakes routes} \usage{ new_regexp(x) } \arguments{ \item{x}{String scalar containing a regular expression.} } \value{ String with class \code{webfakes_regexp}. } \description{ Note that webfakes uses PERL regular expressions. } \details{ As R does not have data type or class for regular expressions, you can use \code{new_regexp()} to mark a string as a regular expression, when adding routes. } \examples{ new_regexp("^/api/match/(?.*)$") } \seealso{ The 'Path specification' and 'Path parameters' chapters of the manual of \code{\link[=new_app]{new_app()}}. } webfakes/man/webfakes_request.Rd0000644000176200001440000000376614172041777016470 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/request.R \name{webfakes_request} \alias{webfakes_request} \title{A webfakes request object} \description{ webfakes creates a \code{webfakes_request} object for every incoming HTTP request. This object is passed to every matched route and middleware, until the response is sent. It has reference semantics, so handlers can modify it. } \details{ Fields and methods: \itemize{ \item \code{app}: The \code{webfakes_app} object itself. \item \code{headers}: Named list of HTTP request headers. \item \code{hostname}: The Host header, the server hostname and maybe port. \item \code{method}: HTTP method. \item \code{path}: Server path. \item \code{protocol}: \code{"http"} or \code{"https"}. \item \code{query_string}: The raw query string, without the starting \verb{?}. \item \code{query}: Parsed query parameters in a named list. \item \code{remote_addr}: String, the domain name or IP address of the client. webfakes runs on the localhost, so this is \verb{127.0.0.1}. \item \code{url}: The full URL of the request. \item \code{get_header(field)}: Function to query a request header. Returns \code{NULL} if the header is not present. } Body parsing middleware adds additional fields to the request object. See \code{\link[=mw_raw]{mw_raw()}}, \code{\link[=mw_text]{mw_text()}}, \code{\link[=mw_json]{mw_json()}}, \code{\link[=mw_multipart]{mw_multipart()}} and \code{\link[=mw_urlencoded]{mw_urlencoded()}}. } \examples{ # This is how you can see the request and response objects: app <- new_app() app$get("/", function(req, res) { browser() res$send("done") }) app # Now start this app on a port: # app$listen(3000) # and connect to it from a web browser: http://127.0.0.1:3000 # You can also use another R session to connect: # httr::GET("http://127.0.0.1:3000") # or the command line curl tool: # curl -v http://127.0.0.1:3000 # The app will stop while processing the request. } \seealso{ \link{webfakes_response} for the webfakes response object. } webfakes/man/mw_multipart.Rd0000644000176200001440000000161014740243712015631 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/mw-multipart.R \name{mw_multipart} \alias{mw_multipart} \title{Parse a multipart HTTP request body} \usage{ mw_multipart(type = "multipart/form-data") } \arguments{ \item{type}{Content type to match before parsing. If it does not match, then the request object is not modified.} } \value{ Handler function. } \description{ Adds the parsed form fields in the \code{form} element of the request and the parsed files to the \code{files} element. } \examples{ app <- new_app() app$use(mw_multipart()) app } \seealso{ Other middleware: \code{\link{mw_cgi}()}, \code{\link{mw_cookie_parser}()}, \code{\link{mw_etag}()}, \code{\link{mw_json}()}, \code{\link{mw_log}()}, \code{\link{mw_range_parser}()}, \code{\link{mw_raw}()}, \code{\link{mw_static}()}, \code{\link{mw_text}()}, \code{\link{mw_urlencoded}()} } \concept{middleware} webfakes/man/new_app_process.Rd0000644000176200001440000000606414312576030016301 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/app-process.R \name{new_app_process} \alias{new_app_process} \alias{webfakes_app_process} \title{Run a webfakes app in another process} \usage{ new_app_process( app, port = NULL, opts = server_opts(remote = TRUE), start = FALSE, auto_start = TRUE, process_timeout = NULL, callr_opts = NULL ) } \arguments{ \item{app}{\code{webfakes_app} object, the web app to run.} \item{port}{Port to use. By default the OS assigns a port.} \item{opts}{Server options. See \code{\link[=server_opts]{server_opts()}} for the defaults.} \item{start}{Whether to start the web server immediately. If this is \code{FALSE}, and \code{auto_start} is \code{TRUE}, then it is started as neeed.} \item{auto_start}{Whether to start the web server process automatically. If \code{TRUE} and the process is not running, then \verb{$start()}, \verb{$get_port()} and \verb{$url()} start the process.} \item{process_timeout}{How long to wait for the subprocess to start, in milliseconds.} \item{callr_opts}{Options to pass to \code{\link[callr:r_session_options]{callr::r_session_options()}} when setting up the subprocess.} } \value{ A \code{webfakes_app_process} object. \subsection{Methods}{ The \code{webfakes_app_process} class has the following methods: \if{html}{\out{
}}\preformatted{get_app() get_port() stop() get_state() local_env(envvars) url(path = "/", query = NULL) }\if{html}{\out{
}} \itemize{ \item \code{envvars}: Named list of environment variables. The \code{{url}} substring is replaced by the URL of the app. \item \code{path}: Path to return the URL for. \item \code{query}: Additional query parameters, a named list, to add to the URL. } \code{get_app()} returns the app object. \code{get_port()} returns the port the web server is running on. \code{stop()} stops the web server, and also the subprocess. If the error log file is not empty, then it dumps its contents to the screen. \code{get_state()} returns a string, the state of the web server: \itemize{ \item \code{"not running"} the server is not running (because it was stopped already). \item \code{"live"} means that the server is running. \item \code{"dead"} means that the subprocess has quit or crashed. } \code{local_env()} sets the given environment variables for the duration of the app process. It resets them in \verb{$stop()}. Webfakes replaces \code{{url}} in the value of the environment variables with the app URL, so you can set environment variables that point to the app. \code{url()} returns the URL of the web app. You can use the \code{path} parameter to return a specific path. } } \description{ Runs an app in a subprocess, using \link[callr:r_session]{callr::r_session}. } \examples{ app <- new_app() app$get("/foo", function(req, res) { res$send("Hello world!") }) proc <- new_app_process(app) url <- proc$url("/foo") resp <- curl::curl_fetch_memory(url) cat(rawToChar(resp$content)) proc$stop() } \seealso{ \code{\link[=local_app_process]{local_app_process()}} for automatically cleaning up the subprocess. } webfakes/man/mw_cookie_parser.Rd0000644000176200001440000000143214740243712016437 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/mw-cookie-parser.R \name{mw_cookie_parser} \alias{mw_cookie_parser} \title{Middleware to parse Cookies} \usage{ mw_cookie_parser() } \value{ Handler function. } \description{ Adds the cookies as the \code{cookies} element of the request object. } \details{ It ignores cookies in an invalid format. It ignores duplicate cookies: if two cookies have the same name, only the first one is included. } \seealso{ Other middleware: \code{\link{mw_cgi}()}, \code{\link{mw_etag}()}, \code{\link{mw_json}()}, \code{\link{mw_log}()}, \code{\link{mw_multipart}()}, \code{\link{mw_range_parser}()}, \code{\link{mw_raw}()}, \code{\link{mw_static}()}, \code{\link{mw_text}()}, \code{\link{mw_urlencoded}()} } \concept{middleware} webfakes/man/oauth2_third_party_app.Rd0000644000176200001440000000615214172041777017574 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/oauth.R \name{oauth2_third_party_app} \alias{oauth2_third_party_app} \title{App representing the third-party app} \usage{ oauth2_third_party_app(name = "Third-Party app") } \arguments{ \item{name}{Name of the third-party app} } \value{ webfakes app } \description{ The webfakes package comes with two fake apps that allow to imitate the OAuth2.0 flow in your test cases. (See \href{https://aaronparecki.com/oauth-2-simplified/}{Aaron Parecki’s tutorial} for a good introduction to OAuth2.0.) One app (\code{oauth2_resource_app()}) is the API server that serves both as the resource and provides authorization. \code{oauth2_third_party_app()} plays the role of the third-party app. They are useful when testing or demonstrating code handling OAuth2.0 authorization, token caching, etc. in a package. The apps can be used in your tests directly, or you could adapt one or both of them to better mimic a particular OAuth2.0 flow. } \details{ Endpoints: \itemize{ \item \code{POST /login/config} Use this endpoint to configure the client ID and the client secret of the app, received from \code{\link[=oauth2_resource_app]{oauth2_resource_app()}} (or another resource app). You need to send in a JSON or URL encoded body: \itemize{ \item \code{auth_url}, the authorization URL of the resource app. \item \code{token_url}, the token URL of the resource app. \item \code{client_id}, the client ID, received from the resource app. \item \code{client_secret} the client secret, received from the resource app. } \item \code{GET /login} Use this endpoint to start the login process. It will redirect to the resource app for authorization and after the OAuth2.0 dance to \verb{/login/redirect}. \item \code{GET /login/redirect}, \code{POST /login/redirect} This is the redirect URI of the third party app. (Some HTTP clients redirect a \code{POST} to a \code{GET}, others don't, so it has both.) This endpoint is used by the resource app, and it received the \code{code} that can be exchanged to an access token and the \code{state} which was generated in \verb{/login}. It contacts the resource app to get an access token, and then stores the token in its \code{app$locals} local variables. It fails with HTTP code 500 if it cannot obtain an access token. On success it returns a JSON dictionary with \code{access_token}, \code{expiry} and \code{refresh_token} (optionally) by default. This behavior can be changed by redefining the \code{app$redirect_hook()} function. \item \code{GET /locals} returns the tokens that were obtained from the resource app. \item \code{GET /data} is an endpoint that uses the obtained token(s) to connect to the \verb{/data} endpoint of the resource app. The \verb{/data} endpoint of the resource app needs authorization. It responds with the response of the resource app. It tries to refresh the access token of the app if needed. } For more details see \code{vignette("oauth", package = "webfakes")}. } \seealso{ Other OAuth2.0 functions: \code{\link{oauth2_httr_login}()}, \code{\link{oauth2_login}()}, \code{\link{oauth2_resource_app}()} } \concept{OAuth2.0 functions} webfakes/man/mw_cgi.Rd0000644000176200001440000000534314740430520014354 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/mw-cgi.R \name{mw_cgi} \alias{mw_cgi} \title{Middleware that calls a CGI script} \usage{ mw_cgi(command, args = character(), timeout = as.difftime(Inf, units = "secs")) } \arguments{ \item{command}{External command to run.} \item{args}{Arguments to pass to the external command.} \item{timeout}{Timeout for the external command. If the command does not terminate in time, the web server kills it and returns an 500 response.} } \value{ A function with signature \if{html}{\out{
}}\preformatted{function(req, res, env = character()) }\if{html}{\out{
}} See \href{https://datatracker.ietf.org/doc/html/rfc3875}{RFC 3875} for details on the CGI protocol. The request body (if any) is passed to the external command as standard intput. \code{mw_cgi()} sets \code{CONTENT_LENGTH}, \code{CONTENT_TYPE}, \code{GATEWAY_INTERFACE}, \code{PATH_INFO}, \code{QUERY_STRING}, \code{REMOTE_ADDR}, \code{REMOTE_HOST}, \code{REMOTE_USER}, \code{REQUEST_METHOD}, \code{SERVER_NAME}, \code{SERVER_PORT}, \code{SERVER_PROTOCOL}, \code{SERVER_SOFTEWARE}. It does not currently set the \code{AUTH_TYPE}, \code{PATH_TRANSLATED}, \code{REMOTE_IDENT}, \code{SCRIPT_NAME} environment variables. The standard output of the external command is used to set the response status code, the response headers and the response body. Example output from git's CGI: \if{html}{\out{
}}\preformatted{Status: 200 OK Expires: Fri, 01 Jan 1980 00:00:00 GMT Pragma: no-cache Cache-Control: no-cache, max-age=0, must-revalidate Content-Type: application/x-git-upload-pack-advertisement 000eversion 2 0015agent=git/2.42.0 0013ls-refs=unborn 0020fetch=shallow wait-for-done 0012server-option 0017object-format=sha1 0010object-info 0000 }\if{html}{\out{
}} } \description{ You can use it as an unconditional middleware in \code{app$use()}, as a handler on \code{app$get()}, \code{app$post()}, etc., or you can call it from a handler. See examples below. } \examples{ app <- new_app() app$use(mw_cgi("echo", "Status: 200\n\nHello")) app app2 <- new_app() app2$get("/greet", mw_cgi("echo", "Status: 200\n\nHello")) app2 # Using `mw_cgi()` in a handler, you can pass extra environment variables app3 <- new_app() cgi <- mw_cgi("echo", "Status: 200\n\nHello") app2$get("/greet", function(req, res) { cgi(req, res, env = c("EXTRA_VAR" = "EXTRA_VALUE")) }) app3 } \seealso{ Other middleware: \code{\link{mw_cookie_parser}()}, \code{\link{mw_etag}()}, \code{\link{mw_json}()}, \code{\link{mw_log}()}, \code{\link{mw_multipart}()}, \code{\link{mw_range_parser}()}, \code{\link{mw_raw}()}, \code{\link{mw_static}()}, \code{\link{mw_text}()}, \code{\link{mw_urlencoded}()} } \concept{middleware} webfakes/man/mw_json.Rd0000644000176200001440000000212314740243712014561 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/mw-json.R \name{mw_json} \alias{mw_json} \title{Middleware to parse a JSON body} \usage{ mw_json(type = "application/json", simplifyVector = FALSE, ...) } \arguments{ \item{type}{Content type to match before parsing. If it does not match, then the request object is not modified.} \item{simplifyVector}{Whether to simplify lists to vectors, passed to \code{\link[jsonlite:fromJSON]{jsonlite::fromJSON()}}.} \item{...}{Arguments to pass to \code{\link[jsonlite:fromJSON]{jsonlite::fromJSON()}}, that performs the JSON parsing.} } \value{ Handler function. } \description{ Adds the parsed object as the \code{json} element of the request object. } \examples{ app <- new_app() app$use(mw_json()) app } \seealso{ Other middleware: \code{\link{mw_cgi}()}, \code{\link{mw_cookie_parser}()}, \code{\link{mw_etag}()}, \code{\link{mw_log}()}, \code{\link{mw_multipart}()}, \code{\link{mw_range_parser}()}, \code{\link{mw_raw}()}, \code{\link{mw_static}()}, \code{\link{mw_text}()}, \code{\link{mw_urlencoded}()} } \concept{middleware} webfakes/man/git_app.Rd0000644000176200001440000000224314740243712014533 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/git-app.R \name{git_app} \alias{git_app} \title{Web app that acts as a git http server} \usage{ git_app( git_root, git_cmd = "git", git_timeout = as.difftime(1, units = "mins"), filter = TRUE, cleanup = TRUE ) } \arguments{ \item{git_root}{Path to the root of the directory tree to be served.} \item{git_cmd}{Command to call, by default it is \code{"git"}. It may also be a full path to git.} \item{git_timeout}{A \code{difftime} object, time limit for the git command.} \item{filter}{Whether to support the \code{filter} capability in the server.} \item{cleanup}{Whether to clean up \code{git_root} when the app is garbage collected.} } \description{ It is useful for tests that need an HTTP git server. } \examples{ \dontshow{if (FALSE) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} dir.create(tmp <- tempfile()) setwd(tmp) system("git clone --bare https://github.com/cran/crayon") system("git clone --bare https://github.com/cran/glue") app <- git_app(tmp) git <- new_app_process(app) system(paste("git ls-remote", git$url("/crayon"))) \dontshow{\}) # examplesIf} } webfakes/man/oauth2_httr_login.Rd0000644000176200001440000000250314172041777016550 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/oauth.R \name{oauth2_httr_login} \alias{oauth2_httr_login} \title{Helper function to use httr's OAuth2.0 functions non-interactively, e.g. in test cases} \usage{ oauth2_httr_login(expr) } \arguments{ \item{expr}{Expression that calls \code{\link[httr:oauth2.0_token]{httr::oauth2.0_token()}}, either directly, or indirectly.} } \value{ The return value of \code{expr}. } \description{ To perform an automatic acknowledgement and log in for a local OAuth2.0 app, run by httr, wrap the expression that obtains the OAuth2.0 token in \code{oauth2_httr_login()}. } \details{ In interactive sessions, \code{oauth2_httr_login()} overrides the \code{browser} option, and when httr opens a browser page, it calls \code{\link[=oauth2_login]{oauth2_login()}} in a subprocess. In non-interactive sessions, httr does not open a browser page, only messages the user to do it manually. \code{oauth2_httr_login()} listens for these messages, and calls \code{\link[=oauth2_login]{oauth2_login()}} in a subprocess. } \seealso{ See \code{?vignette("oauth", package = "webfakes")} for a case study that uses this function. Other OAuth2.0 functions: \code{\link{oauth2_login}()}, \code{\link{oauth2_resource_app}()}, \code{\link{oauth2_third_party_app}()} } \concept{OAuth2.0 functions} webfakes/man/server_opts.Rd0000644000176200001440000000520014740243712015457 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/server.R \name{server_opts} \alias{server_opts} \title{Webfakes web server options} \usage{ server_opts( remote = FALSE, port = NULL, num_threads = 1, interfaces = "127.0.0.1", enable_keep_alive = FALSE, access_log_file = remote, error_log_file = TRUE, tcp_nodelay = FALSE, throttle = Inf, decode_url = TRUE ) } \arguments{ \item{remote}{Meta-option. If set to \code{TRUE}, webfakes uses slightly different defaults, that are more appropriate for a background server process.} \item{port}{Port to start the web server on. Defaults to a randomly chosen port.} \item{num_threads}{Number of request handler threads to use. Typically you don't need more than one thread, unless you run test cases in parallel or you make concurrent HTTP requests.} \item{interfaces}{The network interfaces to listen on. Being a test web server, it defaults to the localhost. Only bind to a public interface if you know what you are doing. webfakes was not designed to serve public web pages.} \item{enable_keep_alive}{Whether the server keeps connections alive.} \item{access_log_file}{\code{TRUE}, \code{FALSE}, or a path. See 'Logging' below.} \item{error_log_file}{\code{TRUE}, \code{FALSE}, or a path. See 'Logging' below.} \item{tcp_nodelay}{if \code{TRUE} then packages will be sent as soon as possible, instead of waiting for a full buffer or timeout to occur.} \item{throttle}{Limit download speed for clients. If not \code{Inf}, then it is the maximum number of bytes per second, that is sent to as connection.} \item{decode_url}{Whether the server should automatically decode URL-encodded URLs. If \code{TRUE} (the default), \verb{/foo\%2fbar} will be converted to \verb{/foo/bar} automatically. If \code{FALSE}, URLs as not URL-decoded.} } \value{ List of options that can be passed to \code{webfakes_app$listen()} (see \code{\link[=new_app]{new_app()}}), and \code{\link[=new_app_process]{new_app_process()}}. } \description{ Webfakes web server options } \section{Logging}{ \itemize{ \item For \code{access_log_file}, \code{TRUE} means \verb{/access.log}. \item For \code{error_log_file}, \code{TRUE} means \verb{/error.log}. } \verb{} is set to the contents of the \code{WEBFAKES_LOG_DIR} environment variable, if it is set. Otherwise it is set to \verb{/webfakes} for local apps and \verb{//webfakes} for remote apps (started with \code{new_app_procss()}). \verb{} is the session temporary directory of the \emph{main process}. \verb{} is the process id of the subprocess. } \examples{ # See the defaults server_opts() } webfakes/man/tmpl_glue.Rd0000644000176200001440000000306114172041777015105 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/tmpl-glue.R \name{tmpl_glue} \alias{tmpl_glue} \title{glue based template engine} \usage{ tmpl_glue( sep = "", open = "{", close = "}", na = "NA", transformer = NULL, trim = TRUE ) } \arguments{ \item{sep}{Separator used to separate elements.} \item{open}{The opening delimiter. Doubling the full delimiter escapes it.} \item{close}{The closing delimiter. Doubling the full delimiter escapes it.} \item{na}{Value to replace NA values with. If \code{NULL} missing values are propagated, that is an \code{NA} result will cause \code{NA} output. Otherwise the value is replaced by the value of \code{na}.} \item{transformer}{A function taking three parameters \code{code}, \code{envir} and \code{data} used to transform the output of each block before during or after evaluation.} \item{trim}{Whether to trim the input template with \code{\link[glue:trim]{glue::trim()}} or not.} } \value{ Template function. } \description{ Use this template engine to create pages with glue templates. See \code{\link[glue:glue]{glue::glue()}} for the syntax. } \examples{ # See th 'hello' app at hello_root <- system.file(package = "webfakes", "examples", "hello") hello_root app <- new_app() app$engine("txt", tmpl_glue()) app$use(mw_log()) app$get("/view", function(req, res) { txt <- res$render("test") res$ set_type("text/plain")$ send(txt) }) # Switch to the app's root: setwd(hello_root) # Now start the app with: app$listen(3000L) # Or start it in another process: new_process(app) } webfakes/man/new_app.Rd0000644000176200001440000003335314740243712014547 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/app.R \name{new_app} \alias{new_app} \alias{webfakes_app} \title{Create a new web application} \usage{ new_app() } \value{ A new \code{webfakes_app}. } \description{ Create a new web application } \details{ The typical workflow of creating a web application is: \enumerate{ \item Create a \code{webfakes_app} object with \code{new_app()}. \item Add middleware and/or routes to it. \item Start is with the \code{webfakes_app$listen()} method, or start it in another process with \code{\link[=new_app_process]{new_app_process()}}. \item Make queries to the web app. \item Stop it via \code{CTRL+C} / \code{ESC}, or, if it is running in another process, with the \verb{$stop()} method of \code{\link[=new_app_process]{new_app_process()}}. } A web application can be \itemize{ \item restarted, \item saved to disk, \item copied to another process using the callr package, or a similar way, \item embedded into a package, \item extended by simply adding new routes and/or middleware. } The webfakes API is very much influenced by the \href{http://expressjs.com/}{express.js} project. \subsection{Create web app objects}{ \if{html}{\out{
}}\preformatted{new_app() }\if{html}{\out{
}} \code{new_app()} returns a \code{webfakes_app} object the has the methods listed on this page. An app is an environment with S3 class \code{webfakes_app}. } \subsection{The handler stack}{ An app has a stack of handlers. Each handler can be a route or middleware. The differences between the two are: \itemize{ \item A route is bound to one or more paths on the web server. Middleware is not (currently) bound to paths, but run for all paths. \item A route is usually (but not always) the end of the handler stack for a request. I.e. a route takes care of sending out the response to the request. Middleware typically performs some action on the request or the response, and then the next handler in the stack is invoked. } } \subsection{Routes}{ The following methods define routes. Each method corresponds to the HTTP verb with the same name, except for \code{app$all()}, which creates a route for all HTTP methods. \if{html}{\out{
}}\preformatted{app$all(path, ...) app$delete(path, ...) app$get(path, ...) app$head(path, ...) app$patch(path, ...) app$post(path, ...) app$put(path, ...) ... (see list below) }\if{html}{\out{
}} \itemize{ \item \code{path} is a path specification, see 'Path specification' below. \item \code{...} is one or more handler functions. These will be placed in the handler stack, and called if they match an incoming HTTP request. See 'Handler functions' below. } webfakes also has methods for the less frequently used HTTP verbs: \code{CONNECT}, \code{MKCOL}, \code{OPTIONS}, \code{PROPFIND}, \code{REPORT}. (The method names are always in lowercase.) If a request is not handled by any routes (or handler functions in general), then webfakes will send a simple HTTP 404 response. } \subsection{Middleware}{ \code{app$use()} adds a middleware to the handler stack. A middleware is a handler function, see 'Handler functions' below. webfakes comes with middleware to perform common tasks: \itemize{ \item \code{\link[=mw_cookie_parser]{mw_cookie_parser()}} parses \code{Cookie} headers. \item \code{\link[=mw_etag]{mw_etag()}} adds an \code{ETag} header to the response. \item \code{\link[=mw_json]{mw_json()}} parses JSON request bodies. \item \code{\link[=mw_log]{mw_log()}} logs each requests to standard output, or another connection. \item \code{\link[=mw_multipart]{mw_multipart()}} parses multipart request bodies. \item \code{\link[=mw_range_parser]{mw_range_parser()}} parses \code{Range} headers. \item \code{\link[=mw_raw]{mw_raw()}} parses raw request bodies. \item \code{\link[=mw_static]{mw_static()}} serves static files from a directory. \item \code{\link[=mw_text]{mw_text()}} parses plain text request bodies. \item \code{\link[=mw_urlencoded]{mw_urlencoded()}} parses URL encoded request bodies. } \if{html}{\out{
}}\preformatted{app$use(..., .first = FALSE) }\if{html}{\out{
}} \itemize{ \item \code{...} is a set of (middleware) handler functions. They are added to the handler stack, and called for every HTTP request. (Unless an HTTP response is created before reaching this point in the handler stack.) \item \code{.first} set to \code{TRUE} is you want to add the handler function to the bottom of the stack. } } \subsection{Handler functions}{ A handler function is a route or middleware. A handler function is called by webfakes with the incoming HTTP request and the outgoing HTTP response objects (being built) as arguments. The handler function may query and modify the members of the request and/or the response object. If it returns the string \code{"next"}, then it is \emph{not} a terminal handler, and once it returns, webfakes will move on to call the next handler in the stack. A typical route: \if{html}{\out{
}}\preformatted{app$get("/user/:id", function(req, res) \{ id <- req$params$id ... res$ set_status(200L)$ set_header("X-Custom-Header", "foobar")$ send_json(response, auto_unbox = TRUE) \}) }\if{html}{\out{
}} \itemize{ \item The handler belongs to an API path, which is a wildcard path in this case. It matches \verb{/user/alice}, \verb{/user/bob}, etc. The handler will be only called for GET methods and matching API paths. \item The handler receives the request (\code{req}) and the response (\code{res}). \item It sets the HTTP status, additional headers, and sends the data. (In this case the \code{webfakes_response$send_json()} method automatically converts \code{response} to JSON and sets the \code{Content-Type} and \code{Content-Length} headers. \item This is a terminal handler, because it does \emph{not} return \code{"next"}. Once this handler function returns, webfakes will send out the HTTP response. } A typical middleware: \if{html}{\out{
}}\preformatted{app$use(function(req, res) \{ ... "next" \}) }\if{html}{\out{
}} \itemize{ \item There is no HTTP method and API path here, webfakes will call the handler for each HTTP request. \item This is not a terminal handler, it does return \code{"next"}, so after it returns webfakes will look for the next handler in the stack. } } \subsection{Errors}{ If a handler function throws an error, then the web server will return a HTTP 500 \code{text/plain} response, with the error message as the response body. } \subsection{Request and response objects}{ See \link{webfakes_request} and \link{webfakes_response} for the methods of the request and response objects. } \subsection{Path specification}{ Routes are associated with one or more API paths. A path specification can be \itemize{ \item A "plain" (i.e. without parameters) string. (E.g. \code{"/list"}.) \item A parameterized string. (E.g. \code{"/user/:id"}.) \item A regular expression created via \code{\link[=new_regexp]{new_regexp()}} function. \item A list or character vector of the previous ones. (Regular expressions must be in a list.) } } \subsection{Path parameters}{ Paths that are specified as parameterized strings or regular expressions can have parameters. For parameterized strings the keys may contain letters, numbers and underscores. When webfakes matches an API path to a handler with a parameterized string path, the parameters will be added to the request, as \code{params}. I.e. in the handler function (and subsequent handler functions, if the current one is not terminal), they are available in the \code{req$params} list. For regular expressions, capture groups are also added as parameters. It is best to use named capture groups, so that the parameters are in a named list. If the path of the handler is a list of parameterized strings or regular expressions, the parameters are set according to the first matching one. } \subsection{Templates}{ webfakes supports templates, using any template engine. It comes with a template engine that uses the glue package, see \code{\link[=tmpl_glue]{tmpl_glue()}}. \code{app$engine()} registers a template engine, for a certain file extension. The \verb{$render()} method of \link{webfakes_response} can be called from the handler function to evaluate a template from a file. \if{html}{\out{
}}\preformatted{app$engine(ext, engine) }\if{html}{\out{
}} \itemize{ \item \code{ext}: the file extension for which the template engine is added. It should not contain the dot. E.g. \verb{"html"', }"brew"`. \item \code{engine}: the template engine, a function that takes the file path (\code{path}) of the template, and a list of local variables (\code{locals}) that can be used in the template. It should return the result. } An example template engine that uses glue might look like this: \if{html}{\out{
}}\preformatted{app$engine("txt", function(path, locals) \{ txt <- readChar(path, nchars = file.size(path)) glue::glue_data(locals, txt) \}) }\if{html}{\out{
}} (The built-in \code{\link[=tmpl_glue]{tmpl_glue()}} engine has more features.) This template engine can be used in a handler: \if{html}{\out{
}}\preformatted{app$get("/view", function(req, res) \{ txt <- res$render("test") res$ set_type("text/plain")$ send(txt) \}) }\if{html}{\out{
}} The location of the templates can be set using the \code{views} configuration parameter, see the \verb{$set_config()} method below. In the template, the variables passed in as \code{locals}, and also the response local variables (see \code{locals} in \link{webfakes_response}), are available. } \subsection{Starting and stopping}{ \if{html}{\out{
}}\preformatted{app$listen(port = NULL, opts = server_opts(), cleanup = TRUE) }\if{html}{\out{
}} \itemize{ \item \code{port}: port to listen on. When \code{NULL}, the operating system will automatically select a free port. \item \code{opts}: options to the web server. See \code{\link[=server_opts]{server_opts()}} for the list of options and their default values. \item \code{cleanup}: stop the server (with an error) if the standard input of the process is closed. This is handy when the app runs in a \code{callr::r_session} subprocess, because it stops the app (and the subprocess) if the main process has terminated. } This method does not return, and can be interrupted with \code{CTRL+C} / \code{ESC} or a SIGINT signal. See \code{\link[=new_app_process]{new_app_process()}} for interrupting an app that is running in another process. When \code{port} is \code{NULL}, the operating system chooses a port where the app will listen. To be able to get the port number programmatically, before the listen method blocks, it advertises the selected port in a \code{webfakes_port} condition, so one can catch it: webfakes by default binds only to the loopback interface at 127.0.0.1, so the webfakes web app is never reachable from the network. \if{html}{\out{
}}\preformatted{withCallingHandlers( app$listen(), "webfakes_port" = function(msg) print(msg$port) ) }\if{html}{\out{
}} } \subsection{Logging}{ webfakes can write an access log that contains an entry for all incoming requests, and also an error log for the errors that happen while the server is running. This is the default behavior for local app (the ones started by \code{app$listen()} and for remote apps (the ones started via \code{new_app_process()}: \itemize{ \item Local apps do not write an access log by default. \item Remote apps write an access log into the \verb{/webfakes//access.log} file, where \verb{} is the session temporary directory of the \emph{main process}, and \verb{} is the process id of the \emph{subprocess}. \item Local apps write an error log to \verb{/webfakes/error.log}, where \verb{} is the session temporary directory of the current process. \item Remote app write an error log to the \verb{/webfakes//error.log}, where \verb{} is the session temporary directory of the \emph{main process} and \verb{} is the process id of the \emph{subprocess}`. } See \code{\link[=server_opts]{server_opts()}} for changing the default logging behavior. } \subsection{Shared app data}{ \if{html}{\out{
}}\preformatted{app$locals }\if{html}{\out{
}} It is often useful to share data between handlers and requests in an app. \code{app$locals} is an environment that supports this. E.g. a middleware that counts the number of requests can be implemented as: \if{html}{\out{
}}\preformatted{app$use(function(req, res) \{ locals <- req$app$locals if (is.null(locals$num)) locals$num <- 0L locals$num <- locals$num + 1L "next" \}) }\if{html}{\out{
}} \link{webfakes_response} objects also have a \code{locals} environment, that is initially populated as a copy of \code{app$locals}. } \subsection{Configuration}{ \if{html}{\out{
}}\preformatted{app$get_config(key) app$set_config(key, value) }\if{html}{\out{
}} \itemize{ \item \code{key}: configuration key. \item \code{value}: configuration value. } Currently used configuration values: \itemize{ \item \code{views}: path where webfakes searches for templates. } } } \examples{ # see example web apps in the `/examples` directory in system.file(package = "webfakes", "examples") app <- new_app() app$use(mw_log()) app$get("/hello", function(req, res) { res$send("Hello there!") }) app$get(new_regexp("^/hi(/.*)?$"), function(req, res) { res$send("Hi indeed!") }) app$post("/hello", function(req, res) { res$send("Got it, thanks!") }) app # Start the app with: app$listen() # Or start it in another R session: new_app_process(app) } \seealso{ \link{webfakes_request} for request objects, \link{webfakes_response} for response objects. } webfakes/man/oauth2_resource_app.Rd0000644000176200001440000001321514306332713017060 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/oauth.R \name{oauth2_resource_app} \alias{oauth2_resource_app} \title{Fake OAuth 2.0 resource and authorization app} \usage{ oauth2_resource_app( access_duration = 3600L, refresh_duration = 7200L, refresh = TRUE, seed = NULL, authorize_endpoint = "/authorize", token_endpoint = "/token" ) } \arguments{ \item{access_duration}{After how many seconds should access tokens expire.} \item{refresh_duration}{After how many seconds should refresh tokens expire (ignored if \code{refresh} is \code{FALSE}).} \item{refresh}{Should a refresh token be returned (logical).} \item{seed}{Random seed used when creating tokens. If \code{NULL}, we rely on R to provide a seed. The app uses its own RNG stream, so it does not affect reproducibility of the tests.} \item{authorize_endpoint}{The authorization endpoint of the resource server. Change this from the default if the real app that you are faking does not use \verb{/authorize}.} \item{token_endpoint}{The endpoint to request tokens. Change this if the real app that you are faking does not use \verb{/token}.} } \value{ a \code{webfakes} app webfakes app } \description{ The webfakes package comes with two fake apps that allow to imitate the OAuth2.0 flow in your test cases. (See \href{https://aaronparecki.com/oauth-2-simplified/}{Aaron Parecki’s tutorial} for a good introduction to OAuth2.0.) One app (\code{oauth2_resource_app()}) is the API server that serves both as the resource and provides authorization. \code{oauth2_third_party_app()} plays the role of the third-party app. They are useful when testing or demonstrating code handling OAuth2.0 authorization, token caching, etc. in a package. The apps can be used in your tests directly, or you could adapt one or both of them to better mimic a particular OAuth2.0 flow. } \details{ The app has the following endpoints: \itemize{ \item \code{GET /register} is the endpoint that you can use to register your third party app. It needs to receive the \code{name} of the third party app, and its \code{redirect_uri} as query parameters, otherwise returns an HTTP 400 error. On success it returns a JSON dictionary with entries \code{name} (the name of the third party app), \code{client_id}, \code{client_secret} and \code{redirect_uri}. \item \code{GET /authorize} is the endpoint where the user of the third party app is sent. You can change the URL of this endpoint with the \code{authorize_endpoint} argument. It needs to receive the \code{client_id} of the third party app, and its correct \code{redirect_uri} as query parameters. It may receive a \code{state} string as well, which can be used by a client to identify the request. Otherwise it generates a random \code{state} string. On error it fails with a HTTP 400 error. On success it returns a simple HTML login page. \item \code{POST /authorize/decision} is the endpoint where the HTML login page generated at \verb{/authorize} connects back to, either with a positive or negative result. The form on the login page will send the \code{state} string and the user's choice in the \code{action} variable. If the user authorized the third party app, then they are redirected to the \code{redirect_uri} of the app, with a temporary \code{code} and the \code{state} string supplied as query parameters. Otherwise a simple HTML page is returned. \item \code{POST /token} is the endpoint where the third party app requests a temporary access token. It is also uses for refreshing an access token with a refresh token. You can change the URL of this endpoint with the \code{token_endpoint} argument. To request a new token or refresh an existing one, the following data must be included in either a JSON or an URL encoded request body: \itemize{ \item \code{grant_type}, this must be \code{authorization_code} for new tokens, and \code{refresh_token} for refreshing. \item \code{code}, this must be the temporary code obtained from the \verb{/authorize/decision} redirection, for new tokens. It is not needed when refreshing. \item \code{client_id} must be the client id of the third party app. \item \code{client_secret} must be the client secret of the third party app. \item \code{redirect_uri} must be the correct redirection URI of the third party app. It is not needed when refreshing tokens. \item \code{refresh_token} must be the refresh token obtained previously, when refreshing a token. It is not needed for new tokens. On success a JSON dictionary is returned with entries: \code{access_token}, \code{expiry} and \code{refresh_token}. (The latter is omitted if the \code{refresh} argument is \code{FALSE}). } \item \code{GET /locals} returns a list of current apps, access tokens and refresh tokens. \item \code{GET /data} is an endpoint that returns a simple JSON response, and needs authorization. } \subsection{Notes}{ \itemize{ \item Using this app in your tests requires the glue package, so you need to put it in \code{Suggests}. \item You can add custom endpoints to the app, as needed. \item If you need authorization in your custom endpoint, call \code{app$is_authorized()} in your handler: \if{html}{\out{
}}\preformatted{if (!app$is_authorized(req, res)) return() }\if{html}{\out{
}} \code{app$is_authorized()} returns an HTTP 401 response if the client is not authorized, so you can simply return from your handler. } For more details see \code{vignette("oauth", package = "webfakes")}. } } \section{\code{oauth2_resource_app()}}{ App representing the API server (resource/authorization) } \seealso{ Other OAuth2.0 functions: \code{\link{oauth2_httr_login}()}, \code{\link{oauth2_login}()}, \code{\link{oauth2_third_party_app}()} } \concept{OAuth2.0 functions} webfakes/man/httpbin_app.Rd0000644000176200001440000000155214740243712015422 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/httpbin.R \name{httpbin_app} \alias{httpbin_app} \title{Generic web app for testing HTTP clients} \usage{ httpbin_app(log = interactive()) } \arguments{ \item{log}{Whether to log requests to the standard output.} } \value{ A \code{webfakes_app}. } \description{ A web app similar to \verb{https://httpbin.org}. See \href{https://webfakes.r-lib.org/httpbin.html}{its specific docs}. You can also see these docs locally, by starting the app: \if{html}{\out{
}}\preformatted{httpbin <- new_app_process(httpbin_app()) browseURL(httpbin$url()) }\if{html}{\out{
}} } \examples{ app <- httpbin_app() proc <- new_app_process(app) url <- proc$url("/get") resp <- curl::curl_fetch_memory(url) curl::parse_headers_list(resp$headers) cat(rawToChar(resp$content)) proc$stop() } webfakes/man/mw_range_parser.Rd0000644000176200001440000000225014740243712016261 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/mw-range-parser.R \name{mw_range_parser} \alias{mw_range_parser} \title{Middleware to parse a Range header} \usage{ mw_range_parser() } \value{ Handler function. } \description{ Adds the requested ranges to the \code{ranges} element of the request object. \code{request$ranges} is a data frame with two columns, \code{from} and \code{to}. Each row corresponds one requested interval. } \details{ When the last \code{n} bytes of the file are requested, the matrix row is set to \code{c(0, -n)}. When all bytes after a \code{p} position are requested, the matrix row is set to \code{c(p, Inf)}. If the intervals overlap, then \code{ranges} is not set, i.e. the \code{Range} header is ignored. If its syntax is invalid or the unit is not \code{bytes}, then the \code{Range} header is ignored. } \seealso{ Other middleware: \code{\link{mw_cgi}()}, \code{\link{mw_cookie_parser}()}, \code{\link{mw_etag}()}, \code{\link{mw_json}()}, \code{\link{mw_log}()}, \code{\link{mw_multipart}()}, \code{\link{mw_raw}()}, \code{\link{mw_static}()}, \code{\link{mw_text}()}, \code{\link{mw_urlencoded}()} } \concept{middleware} webfakes/man/mw_static.Rd0000644000176200001440000000235714740243712015110 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/mw-static.R \name{mw_static} \alias{mw_static} \title{Middleware function to serve static files} \usage{ mw_static(root, set_headers = NULL) } \arguments{ \item{root}{Root path of the served files. Everything under this directory is served automatically. Directory lists are not currently supports.} \item{set_headers}{Callback function to call before a file is served.} } \value{ Handler function. } \description{ The content type of the response is set automatically from the extension of the file. Note that this is a terminal middleware handler function. If a file is served, then the rest of the handler functions will not be called. If a file was not found, however, the rest of the handlers are still called. } \examples{ root <- system.file(package = "webfakes", "examples", "static", "public") app <- new_app() app$use(mw_static(root = root)) app } \seealso{ Other middleware: \code{\link{mw_cgi}()}, \code{\link{mw_cookie_parser}()}, \code{\link{mw_etag}()}, \code{\link{mw_json}()}, \code{\link{mw_log}()}, \code{\link{mw_multipart}()}, \code{\link{mw_range_parser}()}, \code{\link{mw_raw}()}, \code{\link{mw_text}()}, \code{\link{mw_urlencoded}()} } \concept{middleware} webfakes/man/mw_text.Rd0000644000176200001440000000161714740243712014603 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/mw-text.R \name{mw_text} \alias{mw_text} \title{Middleware to parse a plain text body} \usage{ mw_text(default_charset = "utf-8", type = "text/plain") } \arguments{ \item{default_charset}{Encoding to set on the text.} \item{type}{Content type to match before parsing. If it does not match, then the request object is not modified.} } \value{ Handler function. } \description{ Adds the parsed object as the \code{text} element of the request object. } \examples{ app <- new_app() app$use(mw_text()) app } \seealso{ Other middleware: \code{\link{mw_cgi}()}, \code{\link{mw_cookie_parser}()}, \code{\link{mw_etag}()}, \code{\link{mw_json}()}, \code{\link{mw_log}()}, \code{\link{mw_multipart}()}, \code{\link{mw_range_parser}()}, \code{\link{mw_raw}()}, \code{\link{mw_static}()}, \code{\link{mw_urlencoded}()} } \concept{middleware} webfakes/man/mw_log.Rd0000644000176200001440000000256514740243712014403 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/mw-log.R \name{mw_log} \alias{mw_log} \title{Log requests to the standard output or other connection} \usage{ mw_log(format = "dev", stream = "stdout") } \arguments{ \item{format}{Log format. Not implemented currently.} \item{stream}{R connection to log to. \code{"stdout"} means the standard output, \code{"stderr"} is the standard error. You can also supply a connection object, but then you need to be sure that it will be valid when the app is actually running.} } \value{ Handler function. } \description{ A one line log entry for every request. The output looks like this: \if{html}{\out{
}}\preformatted{GET http://127.0.0.1:3000/image 200 3 ms - 4742 }\if{html}{\out{
}} and contains \itemize{ \item the HTTP method, \item the full request URL, \item the HTTP status code of the response, \item how long it took to process the response, in ms, \item and the size of the response body, in bytes. } } \examples{ app <- new_app() app$use(mw_log()) app } \seealso{ Other middleware: \code{\link{mw_cgi}()}, \code{\link{mw_cookie_parser}()}, \code{\link{mw_etag}()}, \code{\link{mw_json}()}, \code{\link{mw_multipart}()}, \code{\link{mw_range_parser}()}, \code{\link{mw_raw}()}, \code{\link{mw_static}()}, \code{\link{mw_text}()}, \code{\link{mw_urlencoded}()} } \concept{middleware} webfakes/man/webfakes_response.Rd0000644000176200001440000001251014740243712016613 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/response.R \name{webfakes_response} \alias{webfakes_response} \title{A webfakes response object} \description{ webfakes creates a \code{webfakes_response} object for every incoming HTTP request. This object is passed to every matched route and middleware, until the HTTP response is sent. It has reference semantics, so handlers can modify it. } \details{ Fields and methods: \itemize{ \item \code{app}: The \code{webfakes_app} object itself. \item \code{req}: The request object. \item \code{headers_sent}: Whether the response headers were already sent out. \item \code{locals}: Local variables, the are shared between the handler functions. This is for the end user, and not for the middlewares. \item \code{delay(secs)}: delay the response for a number of seconds. If a handler calls \code{delay()}, the same handler will be called again, after the specified number of seconds have passed. Use the \code{locals} environment to distinguish between the calls. If you are using \code{delay()}, and want to serve requests in parallel, then you probably need a multi-threaded server, see \code{\link[=server_opts]{server_opts()}}. \item \code{add_header(field, value)}: Add a response header. Note that \code{add_header()} may create duplicate headers. You usually want \code{set_header()}. \item \code{get_header(field)}: Query the currently set response headers. If \code{field} is not present it return \code{NULL}. \item \code{on_response(fun)}: Run the \code{fun} handler function just before the response is sent out. At this point the headers and the body are already properly set. \item \code{redirect(path, status = 302)}: Send a redirect response. It sets the \code{Location} header, and also sends a \code{text/plain} body. \item \code{render(view, locals = list())}: Render a template page. Searches for the \code{view} template page, using all registered engine extensions, and calls the first matching template engine. Returns the filled template. \item \code{send(body)}. Send the specified body. \code{body} can be a raw vector, or HTML or other text. For raw vectors it sets the content type to \code{application/octet-stream}. \item \code{send_json(object = NULL, text = NULL, ...)}: Send a JSON response. Either \code{object} or \code{text} must be given. \code{object} will be converted to JSON using \code{\link[jsonlite:fromJSON]{jsonlite::toJSON()}}. \code{...} are passed to \code{\link[jsonlite:fromJSON]{jsonlite::toJSON()}}. It sets the content type appropriately. \item \code{send_file(path, root = ".")}: Send a file. Set \code{root = "/"} for absolute file names. It sets the content type automatically, based on the extension of the file, if it is not set already. \item \code{send_status(status)}: Send the specified HTTP status code, without a response body. \item \code{send_chunk(data)}: Send a chunk of a response in chunked encoding. The first chunk will automatically send the HTTP response headers. Webfakes will automatically send a final zero-lengh chunk, unless \verb{$delay()} is called. \item \code{set_header(field, value)}: Set a response header. If the headers have been sent out already, then it throws a warning, and does nothing. \item \code{set_status(status)}: Set the response status code. If the headers have been sent out already, then it throws a warning, and does nothing. \item \code{set_type(type)}: Set the response content type. If it contains a \code{/} character then it is set as is, otherwise it is assumed to be a file extension, and the corresponding MIME type is set. If the headers have been sent out already, then it throws a warning, and does nothing. \item \code{add_cookie(name, value, options)}: Adds a cookie to the response. \code{options} is a named list, and may contain: \itemize{ \item \code{domain}: Domain name for the cookie, not set by default. \item \code{expires}: Expiry date in GMT. It must be a POSIXct object, and will be formatted correctly. \item 'http_only': if TRUE, then it sets the 'HttpOnly' attribute, so Javasctipt cannot access the cookie. \item \code{max_age}: Maximum age, in number of seconds. \item \code{path}: Path for the cookie, defaults to "/". \item \code{same_site}: The 'SameSite' cookie attribute. Possible values are "strict", "lax" and "none". \item \code{secure}: if TRUE, then it sets the 'Secure' attribute. } \item \code{clear_cookie(name, options = list())}: clears a cookie. Typically, web browsers will only clear a cookie if the options also match. \item \code{write(data)}: writes (part of) the body of the response. It also sends out the response headers, if they haven't been sent out before. } Usually you need one of the \code{send()} methods, to send out the HTTP response in one go, first the headers, then the body. Alternatively, you can use \verb{$write()} to send the response in parts. } \examples{ # This is how you can see the request and response objects: app <- new_app() app$get("/", function(req, res) { browser() res$send("done") }) app # Now start this app on a port: # app$listen(3000) # and connect to it from a web browser: http://127.0.0.1:3000 # You can also use another R session to connect: # httr::GET("http://127.0.0.1:3000") # or the command line curl tool: # curl -v http://127.0.0.1:3000 # The app will stop while processing the request. } \seealso{ \link{webfakes_request} for the webfakes request object. } webfakes/man/mw_etag.Rd0000644000176200001440000000202014740243712014524 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/mw-etag.R \name{mw_etag} \alias{mw_etag} \title{Middleware that add an \code{ETag} header to the response} \usage{ mw_etag(algorithm = "crc32") } \arguments{ \item{algorithm}{Checksum algorithm to use. Only \code{"crc32"} is implemented currently.} } \value{ Handler function. } \description{ If the response already has an \code{ETag} header, then it is kept. } \details{ This middleware handles the \code{If-None-Match} headers, and it sets the status code of the response to 304 if \code{If-None-Match} matches the \code{ETag}. It also removes the response body in this case. } \examples{ app <- new_app() app$use(mw_etag()) app } \seealso{ Other middleware: \code{\link{mw_cgi}()}, \code{\link{mw_cookie_parser}()}, \code{\link{mw_json}()}, \code{\link{mw_log}()}, \code{\link{mw_multipart}()}, \code{\link{mw_range_parser}()}, \code{\link{mw_raw}()}, \code{\link{mw_static}()}, \code{\link{mw_text}()}, \code{\link{mw_urlencoded}()} } \concept{middleware} webfakes/man/oauth2_login.Rd0000644000176200001440000000170114172041777015506 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/oauth.R \name{oauth2_login} \alias{oauth2_login} \title{Helper function to log in to a third party OAuth2.0 app without a browser} \usage{ oauth2_login(login_url) } \arguments{ \item{login_url}{The login URL of the third party app.} } \value{ A named list with \itemize{ \item \code{login_response} The curl HTTP response object for the login page. \item \code{token_response} The curl HTTP response object for submitting the login page. } } \description{ It works with \code{\link[=oauth2_resource_app]{oauth2_resource_app()}}, and any third party app, including the fake \code{\link[=oauth2_third_party_app]{oauth2_third_party_app()}}. } \details{ See \code{test-oauth.R} in webfakes for an example. } \seealso{ Other OAuth2.0 functions: \code{\link{oauth2_httr_login}()}, \code{\link{oauth2_resource_app}()}, \code{\link{oauth2_third_party_app}()} } \concept{OAuth2.0 functions} webfakes/man/mw_raw.Rd0000644000176200001440000000150214740243712014401 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/mw-raw.R \name{mw_raw} \alias{mw_raw} \title{Middleware to read the raw body of a request} \usage{ mw_raw(type = "application/octet-stream") } \arguments{ \item{type}{Content type to match. If it does not match, then the request object is not modified.} } \value{ Handler function. } \description{ Adds the raw body, as a raw object to the \code{raw} field of the request. } \examples{ app <- new_app() app$use(mw_raw()) app } \seealso{ Other middleware: \code{\link{mw_cgi}()}, \code{\link{mw_cookie_parser}()}, \code{\link{mw_etag}()}, \code{\link{mw_json}()}, \code{\link{mw_log}()}, \code{\link{mw_multipart}()}, \code{\link{mw_range_parser}()}, \code{\link{mw_static}()}, \code{\link{mw_text}()}, \code{\link{mw_urlencoded}()} } \concept{middleware} webfakes/man/http_time_stamp.Rd0000644000176200001440000000065114740243712016312 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/utils.R \name{http_time_stamp} \alias{http_time_stamp} \title{Format a time stamp for HTTP} \usage{ http_time_stamp(t = Sys.time()) } \arguments{ \item{t}{Date-time value to format, defaults to the current date and time. It must be a POSIXct object.} } \value{ Character vector, formatted date-time. } \description{ Format a time stamp for HTTP } webfakes/man/mw_urlencoded.Rd0000644000176200001440000000163514740243712015743 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/mw-urlencoded.R \name{mw_urlencoded} \alias{mw_urlencoded} \title{Middleware to parse an url-encoded request body} \usage{ mw_urlencoded(type = "application/x-www-form-urlencoded") } \arguments{ \item{type}{Content type to match before parsing. If it does not match, then the request object is not modified.} } \value{ Handler function. } \description{ This is typically data from a form. The parsed data is added as the \code{form} element of the request object. } \examples{ app <- new_app() app$use(mw_urlencoded()) app } \seealso{ Other middleware: \code{\link{mw_cgi}()}, \code{\link{mw_cookie_parser}()}, \code{\link{mw_etag}()}, \code{\link{mw_json}()}, \code{\link{mw_log}()}, \code{\link{mw_multipart}()}, \code{\link{mw_range_parser}()}, \code{\link{mw_raw}()}, \code{\link{mw_static}()}, \code{\link{mw_text}()} } \concept{middleware} webfakes/man/webfakes-package.Rd0000644000176200001440000000254014740243712016270 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/webfakes-package.R \docType{package} \name{webfakes-package} \alias{webfakes} \alias{webfakes-package} \title{webfakes: Fake Web Apps for HTTP Testing} \description{ Create a web app that makes it easier to test web clients without using the internet. It includes a web app framework with path matching, parameters and templates. Can parse various 'HTTP' request bodies. Can send 'JSON' data or files from the disk. Includes a web app that implements the 'httpbin.org' web service. } \seealso{ Useful links: \itemize{ \item \url{https://webfakes.r-lib.org/} \item \url{https://github.com/r-lib/webfakes} \item Report bugs at \url{https://github.com/r-lib/webfakes/issues} } } \author{ \strong{Maintainer}: Gábor Csárdi \email{csardi.gabor@gmail.com} Other contributors: \itemize{ \item Posit Software, PBC [copyright holder, funder] \item Civetweb contributors (see inst/credits/ciwetweb.md) [contributor] \item Redoc contributors (see inst/credits/redoc.md) [contributor] \item L. Peter Deutsch (src/md5.h) [contributor] \item Martin Purschke (src/md5.h) [contributor] \item Aladdin Enterprises (src/md5.h) [copyright holder] \item Maëlle Salmon \email{maelle.salmon@yahoo.se} (\href{https://orcid.org/0000-0002-2815-0399}{ORCID}) [contributor] } } \keyword{internal} webfakes/DESCRIPTION0000644000176200001440000000416614740436161013564 0ustar liggesusersPackage: webfakes Title: Fake Web Apps for HTTP Testing Version: 1.3.2 Authors@R: c( person("Gábor", "Csárdi", , "csardi.gabor@gmail.com", role = c("aut", "cre")), person("Posit Software, PBC", role = c("cph", "fnd")), person(, "Civetweb contributors", role = "ctb", comment = "see inst/credits/ciwetweb.md"), person(, "Redoc contributors", role = "ctb", comment = "see inst/credits/redoc.md"), person("L. Peter", "Deutsch", role = "ctb", comment = "src/md5.h"), person("Martin", "Purschke", role = "ctb", comment = "src/md5.h"), person(, "Aladdin Enterprises", role = "cph", comment = "src/md5.h"), person("Maëlle", "Salmon", , "maelle.salmon@yahoo.se", role = "ctb", comment = c(ORCID = "0000-0002-2815-0399")) ) Description: Create a web app that makes it easier to test web clients without using the internet. It includes a web app framework with path matching, parameters and templates. Can parse various 'HTTP' request bodies. Can send 'JSON' data or files from the disk. Includes a web app that implements the 'httpbin.org' web service. License: MIT + file LICENSE URL: https://webfakes.r-lib.org/, https://github.com/r-lib/webfakes BugReports: https://github.com/r-lib/webfakes/issues Depends: R (>= 3.6) Imports: stats, tools, utils Suggests: brotli, callr, covr, curl, digest, glue, httpuv, httr, jsonlite, processx, testthat (>= 3.0.0), withr, xml2, zip (>= 2.3.0) Config/Needs/website: tidyverse/tidytemplate Config/testthat/edition: 3 Encoding: UTF-8 RoxygenNote: 7.3.2 NeedsCompilation: yes Packaged: 2025-01-11 09:17:50 UTC; gaborcsardi Author: Gábor Csárdi [aut, cre], Posit Software, PBC [cph, fnd], Civetweb contributors [ctb] (see inst/credits/ciwetweb.md), Redoc contributors [ctb] (see inst/credits/redoc.md), L. Peter Deutsch [ctb] (src/md5.h), Martin Purschke [ctb] (src/md5.h), Aladdin Enterprises [cph] (src/md5.h), Maëlle Salmon [ctb] () Maintainer: Gábor Csárdi Repository: CRAN Date/Publication: 2025-01-11 09:40:01 UTC