#define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define RECV_BUF 65536 #define SMALL_BUF 4096 static volatile sig_atomic_t g_stop = 0; static const char *DOC_ROOT = NULL; // current working directory static void on_sigint(int sig){ (void)sig; g_stop = 1; } static long long now_ms(void){ struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); return (long long)ts.tv_sec*1000 + ts.tv_nsec/1000000; } static const char* getenv_default(const char* k, const char* def){ const char* v=getenv(k); return v&&*v? v: def; } // ---- file helpers ---- static int ensure_dir(const char* path){ struct stat st; if(stat(path,&st)==0){ if(S_ISDIR(st.st_mode)) return 0; errno=ENOTDIR; return -1; } if(mkdir(path,0775)==0) return 0; return -1; } static char* path_join(const char* a, const char* b){ size_t la=strlen(a), lb=strlen(b); size_t n=la+1+lb+1; char* r=malloc(n); if(!r) return NULL; snprintf(r,n,"%s/%s",a,b); return r; } static char* data_dir(void){ const char* env = getenv("ARTICLES_DATA_DIR"); if(env && *env){ ensure_dir(env); return strdup(env); } char* d = path_join(DOC_ROOT?DOC_ROOT: ".", "data"); if(d){ ensure_dir(d); } return d; } static char* data_file(void){ char* d = data_dir(); if(!d) return NULL; char* f = path_join(d, "articles.json"); free(d); return f; } // Read entire file into memory (NUL-terminated). Caller frees. static char* read_file_all(const char* path, size_t* out_len){ FILE* f=fopen(path,"rb"); if(!f){ if(out_len) *out_len=0; return NULL; } fseek(f,0,SEEK_END); long sz=ftell(f); if(sz<0) sz=0; fseek(f,0,SEEK_SET); char* buf=malloc((size_t)sz+1); if(!buf){ fclose(f); return NULL; } size_t n=fread(buf,1,(size_t)sz,f); fclose(f); buf[n]='\0'; if(out_len) *out_len=n; return buf; } static int write_file_all(const char* path, const char* data, size_t len){ FILE* f=fopen(path,"wb"); if(!f) return -1; size_t n=fwrite(data,1,len,f); fclose(f); return n==len?0:-1; } // removed unused append_file_line (lint) // ---- JSON helpers (minimal) ---- static char* json_escape(const char* s){ size_t n=0; for(const char* p=s; *p; ++p){ if(*p=='"'||*p=='\\'||*p=='\n'||*p=='\r'||*p=='\t') n+=2; else n++; } char* out=malloc(n+1); if(!out) return NULL; char* w=out; for(const char* p=s; *p; ++p){ if(*p=='"'){ *w++='\\'; *w++='"'; } else if(*p=='\\'){ *w++='\\'; *w++='\\'; } else if(*p=='\n'){ *w++='\\'; *w++='n'; } else if(*p=='\r'){ *w++='\\'; *w++='r'; } else if(*p=='\t'){ *w++='\\'; *w++='t'; } else { *w++=*p; } } *w='\0'; return out; } static const char* skip_ws_commas(const char* p){ while(*p==' '||*p=='\n'||*p=='\r'||*p=='\t'||*p==',') p++; return p; } // Parse a JSON string starting at v (after the opening quote). Returns malloc'd string and sets *after_end to the char after closing quote. static char* parse_json_string_value(const char* v, const char** after_end){ char* out = malloc(strlen(v)+1); if(!out){ *after_end = v; return NULL; } size_t w=0; bool esc=false; const char* p=v; for(; *p; ++p){ char c=*p; if(esc){ if(c=='"'||c=='\\'||c=='/'){ out[w++]=c; } else if(c=='n'){ out[w++]='\n'; } else if(c=='r'){ out[w++]='\r'; } else if(c=='t'){ out[w++]='\t'; } else { out[w++]=c; } esc=false; continue; } if(c=='\\'){ esc=true; continue; } if(c=='"'){ out[w]='\0'; *after_end = p+1; return out; } out[w++]=c; } out[w]='\0'; *after_end = p; return out; // unterminated; best effort } static const char* ensure_quoted_key(const char* key, char** to_free, size_t* out_len){ *to_free=NULL; size_t klen=strlen(key); if(klen==0){ *out_len=0; return key; } if(key[0]=='"'){ *out_len=klen; return key; } char* tmp=malloc(klen+3); if(!tmp){ *out_len=klen; return key; } tmp[0]='"'; memcpy(tmp+1,key,klen); tmp[klen+1]='"'; tmp[klen+2]='\0'; *to_free=tmp; *out_len=klen+2; return tmp; } // Find string field value for key in compact JSON. Returns malloc'd string (unescaped). Empty string if not found. static char* json_get_string(const char* json, const char* key){ char* tmp=NULL; size_t klen=0; const char* qkey=ensure_quoted_key(key,&tmp,&klen); if(klen==0){ return strdup(""); } const char* p=json; while((p=strstr(p,qkey))){ const char* colon=strchr(p+klen,':'); if(!colon){ p+=klen; continue; } const char* v=colon+1; while(*v==' '||*v=='\t') v++; if(*v!='"'){ if(tmp) free(tmp); return strdup(""); } v++; const char* after=NULL; char* out=parse_json_string_value(v,&after); if(out){ if(tmp) free(tmp); return out; } if(tmp) free(tmp); return strdup(""); } if(tmp) free(tmp); return strdup(""); } static long long json_get_number(const char* json, const char* key){ const char* p = strstr(json, key); if(!p) return 0; const char* colon=strchr(p,':'); if(!colon) return 0; const char* v=colon+1; while(*v==' '||*v=='\t') v++; return atoll(v); } // Parse a top-level JSON object and return the string value for a given key (unquoted key name). Caller frees. static char* json_get_top_string(const char* json, const char* key){ size_t keylen=strlen(key); const char* p=json; while(*p && *p!='{') p++; if(*p!='{') return strdup(""); p++; while(*p){ p=skip_ws_commas(p); if(*p=='}' || !*p) break; if(*p!='"'){ while(*p && *p!=',' && *p!='}') p++; continue; } // parse key string p++; const char* ks=p; size_t klen=0; bool esc=false; for(; *p; ++p){ char c=*p; if(esc){ esc=false; continue; } if(c=='\\'){ esc=true; continue; } if(c=='"'){ break; } klen++; } const char* key_start=ks; if(*p=='"') p++; while(*p==' '||*p=='\t') p++; if(*p!=':'){ while(*p && *p!=',' && *p!='}') p++; continue; } p++; while(*p==' '||*p=='\t') p++; int key_match=(klen==keylen) && (strncmp(key_start,key,keylen)==0); if(*p=='"'){ p++; const char* after=NULL; char* out=parse_json_string_value(p,&after); p=after; if(key_match){ return out?out:strdup(""); } else { free(out); } } else { while(*p && *p!=',' && *p!='}') p++; } if(*p==',') p++; } return strdup(""); } // Build object JSON string; caller frees static char* build_article_json(const char* id, const char* title, const char* author, const char* body, const char* thumb, long long createdAt, long long updatedAt){ char *et=json_escape(title?title:""), *eau=json_escape(author?author:""), *eb=json_escape(body?body:""), *eth=json_escape(thumb?thumb:""); if(!et||!eau||!eb||!eth){ free(et); free(eau); free(eb); free(eth); return NULL; } char createdBuf[64]; snprintf(createdBuf,sizeof(createdBuf),"%lld",createdAt); char updated[96]=""; if(updatedAt>0){ snprintf(updated,sizeof(updated),",\"updatedAt\":%lld",updatedAt); } size_t need = strlen(id)+strlen(et)+strlen(eau)+strlen(eb)+strlen(eth)+strlen(createdBuf)+strlen(updated)+80; char* out=malloc(need); if(!out){ free(et); free(eau); free(eb); free(eth); return NULL; } snprintf(out, need, "{\"id\":\"%s\",\"title\":\"%s\",\"author\":\"%s\",\"body\":\"%s\",\"thumb\":\"%s\",\"createdAt\":%s%s}", id, et, eau, eb, eth, createdBuf, updated); free(et); free(eau); free(eb); free(eth); return out; } static char* gen_id(void){ char* out=malloc(17); if(!out) return NULL; unsigned int r = (unsigned int)rand(); long long t=now_ms(); snprintf(out,17,"%08x%08x", (unsigned int)(t&0xffffffff), r); return out; } // ---- data URL handling ---- static int b64val(int c){ if(c>='A'&&c<='Z') return c-'A'; if(c>='a'&&c<='z') return c-'a'+26; if(c>='0'&&c<='9') return c-'0'+52; if(c=='+') return 62; if(c=='/') return 63; return -1; } static unsigned char* base64_decode(const char* s, size_t len, size_t* out_len){ size_t pad=0; if(len>=1 && s[len-1]=='=') pad++; if(len>=2 && s[len-2]=='=') pad++; size_t groups = len/4; size_t outcap = groups*3; if(pad <= outcap) outcap -= pad; else outcap = 0; if(outcap==0) outcap=1; // avoid 0-byte malloc and make room for NUL unsigned char* out = malloc(outcap+1); if(!out) return NULL; size_t w=0; int val=0, valb=-8; for(size_t i=0;i=0){ out[w++] = (unsigned char)((val>>valb)&0xFF); valb-=8; } } if(out_len) *out_len=w; return out; } static int parse_data_url(const char* data_url, char** out_mime, unsigned char** out_bytes, size_t* out_len){ const char* p=data_url; if(strncmp(p,"data:",5)!=0) return -1; p+=5; const char* semi=strchr(p,';'); const char* comma=strchr(p,','); if(!comma) return -1; char* mime=NULL; int is_b64=0; if(semi && semi5 && strncmp(url_start,"data:",5)==0){ if(process_data_url_segment(url_start,url_len,&out,&outcap,&w,&did)){ free(out); return NULL; } } else { if(ensure_capacity(&out,&outcap,w+url_len+1)){ free(out); return NULL; } buf_copy(out,&w,url_start,url_len); } if(ensure_capacity(&out,&outcap,w+1+1)){ free(out); return NULL; } out[w++]='"'; p=url_end+1; } out[w]='\0'; if(changed) *changed=did; return out; } // ---- HTTP helpers ---- // removed unused send_str (lint) static void send_response(int c, int code, const char* status, const char* ctype, const char* body, size_t blen, bool cors){ char head[SMALL_BUF]; int n=snprintf(head,sizeof(head), "HTTP/1.1 %d %s\r\nContent-Type: %s\r\nContent-Length: %zu\r\n%s\r\n", code, status, ctype?ctype:"text/plain", blen, cors? "Access-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: GET,POST,PUT,DELETE,OPTIONS\r\nAccess-Control-Allow-Headers: Content-Type\r\n":""); send(c, head, (size_t)n, 0); if(body && blen) send(c, body, blen, 0); } static const char* guess_mime(const char* path){ const char* ext=strrchr(path,'.'); if(!ext) return "application/octet-stream"; ext++; if(!strcmp(ext,"html")) return "text/html; charset=utf-8"; if(!strcmp(ext,"css")) return "text/css"; if(!strcmp(ext,"js")) return "application/javascript"; if(!strcmp(ext,"png")) return "image/png"; if(!strcmp(ext,"jpg")||!strcmp(ext,"jpeg")) return "image/jpeg"; if(!strcmp(ext,"webp")) return "image/webp"; if(!strcmp(ext,"gif")) return "image/gif"; if(!strcmp(ext,"svg")) return "image/svg+xml"; if(!strcmp(ext,"mp4")) return "video/mp4"; if(!strcmp(ext,"webm")) return "video/webm"; return "application/octet-stream"; } // ---- Data operations (NDJSON-style internal), exposed as JSON array ---- static char* ltrim_dup(const char* s){ while(*s && isspace((unsigned char)*s)) s++; return strdup(s); } // removed unused list_articles_json (lint) static char* find_article_by_id(const char* id){ char* file=data_file(); if(!file) return NULL; size_t n=0; char* content=read_file_all(file,&n); free(file); if(!content) return NULL; char* t=ltrim_dup(content); free(content); if(!t) return NULL; if(t[0] != '['){ free(t); return NULL; } // iterate objects size_t len=strlen(t); size_t i=1; int depth=0; size_t start=0; for(; i=2 && tcontent[clen-1]==']'){ // insert at front bool empty = (clen==2); size_t Lobj=strlen(obj); size_t newlen = 1 + Lobj + (empty?0:1) + (clen-2) + 1; char* out=malloc(newlen+1); if(out){ size_t w=0; out[w++]='['; memcpy(out+w,obj,Lobj); w+=Lobj; if(!empty){ out[w++]=','; memcpy(out+w, tcontent+1, clen-2); w+=(clen-2); } out[w++]=']'; out[w]='\0'; write_file_all(file, out, w); free(out); } } else { // corrupt; overwrite size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); if(arr){ size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr); } } } else { // not an array; overwrite size_t L=strlen(obj); size_t tot=L+2; char* arr=malloc(tot+1); if(arr){ size_t w=0; arr[w++]='['; memcpy(arr+w,obj,L); w+=L; arr[w++]=']'; arr[w]='\0'; write_file_all(file, arr, w); free(arr); } } free(tcontent); } // persist done; return created object JSON free(file); free(id); return obj; } // ---- request handling ---- static const char* ext_from_content_type(const char* ct){ if(!ct) return NULL; if(strstr(ct,"image/png")) return "png"; if(strstr(ct,"image/jpeg")) return "jpg"; if(strstr(ct,"image/jpg")) return "jpg"; if(strstr(ct,"image/webp")) return "webp"; if(strstr(ct,"image/gif")) return "gif"; return NULL; } static const char* get_qparam(const char* path, const char* key){ const char* q=strchr(path,'?'); if(!q) return NULL; q++; size_t klen=strlen(key); while(*q){ if(!strncmp(q,key,klen) && q[klen]=='='){ return q+klen+1; } while(*q && *q!='&') q++; if(*q=='&') q++; } return NULL; } static char* strndup_local(const char* s, size_t n){ char* r=malloc(n+1); if(!r) return NULL; memcpy(r,s,n); r[n]='\0'; return r; } static char* save_upload(const char* body, size_t blen, const char* ext_hint){ if(!body||blen==0) return NULL; const char* updir = "uploads"; ensure_dir(updir); char* name=gen_id(); if(!name) return NULL; const char* ext = (ext_hint&&*ext_hint)? ext_hint: "bin"; size_t need=strlen(updir)+1+strlen(name)+1+strlen(ext)+1; char* path=malloc(need); if(!path){ free(name); return NULL; } snprintf(path,need,"%s/%s.%s", updir, name, ext); free(name); if(write_file_all(path, body, blen)!=0){ free(path); return NULL; } return path; } static void api_get_articles_array(int c){ char* file=data_file(); if(!file){ send_response(c,200,"OK","application/json","[]",2,true); return; } size_t n=0; char* content=read_file_all(file,&n); if(!content){ free(file); send_response(c,200,"OK","application/json","[]",2,true); return; } char* t=ltrim_dup(content); free(content); if(!t){ free(file); send_response(c,200,"OK","application/json","[]",2,true); return; } if(t[0] != '['){ free(file); send_response(c,200,"OK","application/json","[]",2,true); free(t); return; } size_t len=strlen(t); size_t i=1; int depth=0; size_t start=0; size_t count=0, cap=8; char** objs=malloc(cap*sizeof(char*)); if(!objs){ free(t); free(file); send_response(c,500,"Internal Server Error","application/json","",0,true); return; } int changed=0; for(; ibl+1) api_get_article_by_id(c, path+bl+1); else send_response(c,404,"Not Found","application/json","",0,true); return; } if(!strcmp(method,"POST") && !strcmp(path, base)){ api_post_article(c, body); return; } if(!strcmp(method,"PUT") && path[bl]=='/' && strlen(path)>bl+1){ api_put_article(c, path+bl+1, body); return; } if(!strcmp(method,"DELETE") && path[bl]=='/' && strlen(path)>bl+1){ api_delete_article(c, path+bl+1); return; } } if(!strncmp(path, "/api/upload", 12)){ if(!strcmp(method,"POST")){ api_post_upload(c, path, body, blen, content_type); return; } } send_response(c,404,"Not Found","text/plain","Not Found",9,true); } static bool safe_path(const char* p){ if(strstr(p, "..")) return false; return true; } static void normalize_rel_path(const char* path, char* rel, size_t relsz){ if(!strcmp(path,"/")) snprintf(rel, relsz, "%s", "/index.html"); else snprintf(rel, relsz, "%s", path); } static int read_file_to_buf(const char* full, char** out, size_t* n){ FILE* f=fopen(full,"rb"); if(!f) return -1; fseek(f,0,SEEK_END); long sz=ftell(f); fseek(f,0,SEEK_SET); char* buf=malloc((size_t)sz); if(!buf){ fclose(f); return -1; } *n=fread(buf,1,(size_t)sz,f); fclose(f); *out=buf; return 0; } static void send_cached_or_plain(int c, const char* rel, const char* mime, const char* buf, size_t n){ int is_upload=(strncmp(rel,"/uploads/",9)==0); if(is_upload){ char head[SMALL_BUF]; int hlen=snprintf(head,sizeof(head),"HTTP/1.1 200 OK\r\nContent-Type: %s\r\nContent-Length: %zu\r\nCache-Control: public, max-age=31536000, immutable\r\n\r\n",mime,(size_t)n); send(c, head, (size_t)hlen, 0); if(n) send(c, buf, n, 0);} else { send_response(c,200,"OK",mime,buf,n,false);} } static void handle_static(int c, const char* path){ char rel[SMALL_BUF]; normalize_rel_path(path,rel,sizeof(rel)); if(!safe_path(rel)){ send_response(c,403,"Forbidden","text/plain","Forbidden",9,false); return; } char full[SMALL_BUF*2]; snprintf(full,sizeof(full),"%s%s", DOC_ROOT?DOC_ROOT:".", rel); char* buf=NULL; size_t n=0; if(read_file_to_buf(full,&buf,&n)!=0){ send_response(c,404,"Not Found","text/plain","Not Found",9,false); return; } const char* mime=guess_mime(full); send_cached_or_plain(c,rel,mime,buf,n); free(buf); } // removed unused read_headers_into static void parse_request_line(const char* buf, char* method, char* path){ if(sscanf(buf, "%15s %4095s", method, path) < 2){ method[0]='\0'; path[0]='\0'; } } static void parse_headers(const char* buf, size_t* content_length, char* ctype, size_t ctype_sz){ *content_length=0; ctype[0]='\0'; const char* cl=strcasestr(buf,"Content-Length:"); if(cl) *content_length=strtoul(cl+15,NULL,10); const char* ct=strcasestr(buf,"Content-Type:"); if(ct){ ct+=13; while(*ct==' '||*ct=='\t') ct++; size_t i=0; while(*ct && *ct!='\r' && *ct!='\n' && i header_bytes ? total - header_bytes : 0; char* body=NULL; if(content_length){ body=malloc(content_length+1); if(!body) return NULL; size_t off=0; if(have_body){ size_t cpy = have_body>content_length?content_length:have_body; memcpy(body, buf+header_bytes, cpy); off=cpy; } size_t remain=content_length-off; while(remain>0){ ssize_t rr=recv(c, body+off, remain, 0); if(rr<=0) break; off+=rr; remain-=rr; } body[content_length]='\0'; } return body; } static void handle_client(int c){ char buf[RECV_BUF]; ssize_t total=0; // read headers while(true){ ssize_t rcv=recv(c, buf+total, sizeof(buf)-1-total, 0); if(rcv<=0) break; total+=rcv; buf[total]='\0'; if(strstr(buf, "\r\n\r\n")) break; if(total >= (ssize_t)sizeof(buf)-1) break; } if(total<=0){ close(c); return; } char method[16]={0}, path[SMALL_BUF]={0}; parse_request_line(buf,method,path); if(!method[0]){ close(c); return; } size_t content_length=0; char ctype[128]={0}; parse_headers(buf,&content_length,ctype,sizeof(ctype)); char* body=read_body_if_needed(c,buf,(size_t)total,content_length); if(!strncmp(path, "/api/", 5)) handle_api(c, method, path, body, content_length, ctype[0]?ctype:NULL); else if(!strcmp(method,"GET")) handle_static(c, path); else if(!strcmp(method,"OPTIONS")) send_response(c,204,"No Content","text/plain","",0,false); else send_response(c,405,"Method Not Allowed","text/plain","",0,false); free(body); close(c); } int main(int argc, char** argv){ (void)argc; (void)argv; signal(SIGINT, on_sigint); srand((unsigned int)time(NULL)); char cwd[SMALL_BUF]; if(getcwd(cwd,sizeof(cwd))) DOC_ROOT=strdup(cwd); const char* host = getenv_default("HOST","127.0.0.1"); int port = atoi(getenv_default("PORT","8000")); if(port<=0) port=8000; int s = socket(AF_INET, SOCK_STREAM, 0); if(s<0){ perror("socket"); return 1; } int opt=1; setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); struct sockaddr_in addr = {0}; addr.sin_family=AF_INET; addr.sin_port=htons((uint16_t)port); addr.sin_addr.s_addr=inet_addr(host); if(bind(s,(struct sockaddr*)&addr,sizeof(addr))<0){ perror("bind"); close(s); return 1; } if(listen(s,64)<0){ perror("listen"); close(s); return 1; } printf("Serving Mini Articles (C) on http://%s:%d\n", host, port); while(!g_stop){ struct sockaddr_in ca; socklen_t calen=sizeof(ca); int c=accept(s,(struct sockaddr*)&ca,&calen); if(c<0){ if(errno==EINTR) break; perror("accept"); continue; } handle_client(c); } close(s); return 0; }