Files
git/http-backend.c
Ævar Arnfjörð Bjarmason a9ef3e3cfd Add infrastructure for translating Git with gettext
All of the interface messages in Git core are currently hardcoded in
English. Change that by optionally enabling translation of the core C,
Shell and Perl programs via GNU or SunOS gettext. If you set the
appropriate LC_* variables Git will speak your language, provided that
someone has submitted a translation.

If gettext isn't available, or if Git is compiled with
NO_GETTEXT=YesPlease, then Git fall back on its previous behavior of
only speaking English. When using ./configure the autoconf script will
auto-detect if the gettext libraries are installed and act
appropriately.

With NO_GETTEXT=YesPlease gettext support will be #defined away for C
programs. For Shell and Perl programs we rely on the git message
catalog not being available. That's a reasonable assumption since then
the message catalog won't be installed on the system during make
install.

The gettext wrappers that are provided in the patch are only the bare
minimum required to begin translation work. In particular I haven't
added wrappers for the gettext functions that enable plural support,
or those that provide message context (msgctxt).

Those can be added later. The intent is to start with a small subset
and see what we need later, not to start with something that's
unnecessarily large right away.

Implementation and usage notes:

 * General:

   Gettext .mo files will be installed and looked for in the standard
   $(prefix)/share/locale path. GIT_TEXTDOMAINDIR can also be set to
   override that, but that's only intended to be used to test Git
   itself.

 * Perl:

   Perl code that wants to be localized should use the new Git::I18n
   module. It imports a __ function into the caller's package by
   default.

   Instead of using the high level Locale::TextDomain interface I've
   opted to use the low-level (equivalent to the C interface)
   Locale::Messages module, which Locale::TextDomain itself uses.

   Locale::TextDomain does a lot of redundant work we don't need, and
   some of it would potentially introduce bugs. It tries to set the
   $TEXTDOMAIN based on package of the caller, and has its own
   hardcoded paths where it'll search for messages.

   I found it easier just to completely avoid it rather than try to
   circumvent its behavior. In any case, this is an issue wholly
   internal Git::I18N. Its guts can be changed later if that's deemed
   necessary.

   See <AANLkTilYD_NyIZMyj9dHtVk-ylVBfvyxpCC7982LWnVd@mail.gmail.com>
   for a further elaboration on this topic.

 * Shell:

   Shell code that's to be localized should use the new git-sh-i18n
   library. It's just a wrapper for the system's gettext.sh.

   If gettext.sh isn't available we'll fall back gettext(1) if it's
   available. The latter is available without the former on Solaris,
   which has its own non-GNU gettext implementation. We also need to
   emulate eval_gettext() there.

   If neither are present we'll use a dumb printf(1) fall-through
   wrapper.

   I originally tried to detect if the system supported `echo -n' but
   I found this to be a waste of time. My benchmarks on Linux, Solaris
   and FreeBSD reveal that printf(1) is fast enough, especially since
   we aren't calling gettext() from within any tight loops, and
   unlikely to ever do so.

This series has been tested by me on Ubuntu 10.04, Debian testing,
FreeBSD 8.1 and SunOS 5.10, and by others on Mac OS X 10.6.3 (with
Xcode 3.2.2) and openSUSE Factory (11.3, milestone 7).

SunOS has its own non-GNU gettext implementation which this patch
supports, although that may change in the future if it turns out that
we need some GNU libintl features that SunOS doesn't provide.

This patch is based on work by Jeff Epler <jepler@unpythonic.net> who
did the initial Makefile / C work, and a lot of comments from the Git
mailing list, including Jonathan Nieder, Jakub Narebski, Johannes
Sixt, Peter Krefting, Junio C Hamano, Thomas Rast and others.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Tested-By: John Wiegley <johnw@newartisans.com>
Tested-by: Graham Anderson <graham.anderson@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
2010-07-12 07:58:24 -07:00

611 lines
13 KiB
C

#include "cache.h"
#include "refs.h"
#include "pkt-line.h"
#include "object.h"
#include "tag.h"
#include "exec_cmd.h"
#include "run-command.h"
#include "string-list.h"
#include "url.h"
#include "gettext.h"
static const char content_type[] = "Content-Type";
static const char content_length[] = "Content-Length";
static const char last_modified[] = "Last-Modified";
static int getanyfile = 1;
static struct string_list *query_params;
struct rpc_service {
const char *name;
const char *config_name;
signed enabled : 2;
};
static struct rpc_service rpc_service[] = {
{ "upload-pack", "uploadpack", 1 },
{ "receive-pack", "receivepack", -1 },
};
static struct string_list *get_parameters(void)
{
if (!query_params) {
const char *query = getenv("QUERY_STRING");
query_params = xcalloc(1, sizeof(*query_params));
while (query && *query) {
char *name = url_decode_parameter_name(&query);
char *value = url_decode_parameter_value(&query);
struct string_list_item *i;
i = string_list_lookup(query_params, name);
if (!i)
i = string_list_insert(query_params, name);
else
free(i->util);
i->util = value;
}
}
return query_params;
}
static const char *get_parameter(const char *name)
{
struct string_list_item *i;
i = string_list_lookup(get_parameters(), name);
return i ? i->util : NULL;
}
__attribute__((format (printf, 2, 3)))
static void format_write(int fd, const char *fmt, ...)
{
static char buffer[1024];
va_list args;
unsigned n;
va_start(args, fmt);
n = vsnprintf(buffer, sizeof(buffer), fmt, args);
va_end(args);
if (n >= sizeof(buffer))
die("protocol error: impossibly long line");
safe_write(fd, buffer, n);
}
static void http_status(unsigned code, const char *msg)
{
format_write(1, "Status: %u %s\r\n", code, msg);
}
static void hdr_str(const char *name, const char *value)
{
format_write(1, "%s: %s\r\n", name, value);
}
static void hdr_int(const char *name, uintmax_t value)
{
format_write(1, "%s: %" PRIuMAX "\r\n", name, value);
}
static void hdr_date(const char *name, unsigned long when)
{
const char *value = show_date(when, 0, DATE_RFC2822);
hdr_str(name, value);
}
static void hdr_nocache(void)
{
hdr_str("Expires", "Fri, 01 Jan 1980 00:00:00 GMT");
hdr_str("Pragma", "no-cache");
hdr_str("Cache-Control", "no-cache, max-age=0, must-revalidate");
}
static void hdr_cache_forever(void)
{
unsigned long now = time(NULL);
hdr_date("Date", now);
hdr_date("Expires", now + 31536000);
hdr_str("Cache-Control", "public, max-age=31536000");
}
static void end_headers(void)
{
safe_write(1, "\r\n", 2);
}
__attribute__((format (printf, 1, 2)))
static NORETURN void not_found(const char *err, ...)
{
va_list params;
http_status(404, "Not Found");
hdr_nocache();
end_headers();
va_start(params, err);
if (err && *err)
vfprintf(stderr, err, params);
va_end(params);
exit(0);
}
__attribute__((format (printf, 1, 2)))
static NORETURN void forbidden(const char *err, ...)
{
va_list params;
http_status(403, "Forbidden");
hdr_nocache();
end_headers();
va_start(params, err);
if (err && *err)
vfprintf(stderr, err, params);
va_end(params);
exit(0);
}
static void select_getanyfile(void)
{
if (!getanyfile)
forbidden("Unsupported service: getanyfile");
}
static void send_strbuf(const char *type, struct strbuf *buf)
{
hdr_int(content_length, buf->len);
hdr_str(content_type, type);
end_headers();
safe_write(1, buf->buf, buf->len);
}
static void send_local_file(const char *the_type, const char *name)
{
const char *p = git_path("%s", name);
size_t buf_alloc = 8192;
char *buf = xmalloc(buf_alloc);
int fd;
struct stat sb;
fd = open(p, O_RDONLY);
if (fd < 0)
not_found("Cannot open '%s': %s", p, strerror(errno));
if (fstat(fd, &sb) < 0)
die_errno("Cannot stat '%s'", p);
hdr_int(content_length, sb.st_size);
hdr_str(content_type, the_type);
hdr_date(last_modified, sb.st_mtime);
end_headers();
for (;;) {
ssize_t n = xread(fd, buf, buf_alloc);
if (n < 0)
die_errno("Cannot read '%s'", p);
if (!n)
break;
safe_write(1, buf, n);
}
close(fd);
free(buf);
}
static void get_text_file(char *name)
{
select_getanyfile();
hdr_nocache();
send_local_file("text/plain", name);
}
static void get_loose_object(char *name)
{
select_getanyfile();
hdr_cache_forever();
send_local_file("application/x-git-loose-object", name);
}
static void get_pack_file(char *name)
{
select_getanyfile();
hdr_cache_forever();
send_local_file("application/x-git-packed-objects", name);
}
static void get_idx_file(char *name)
{
select_getanyfile();
hdr_cache_forever();
send_local_file("application/x-git-packed-objects-toc", name);
}
static int http_config(const char *var, const char *value, void *cb)
{
if (!strcmp(var, "http.getanyfile")) {
getanyfile = git_config_bool(var, value);
return 0;
}
if (!prefixcmp(var, "http.")) {
int i;
for (i = 0; i < ARRAY_SIZE(rpc_service); i++) {
struct rpc_service *svc = &rpc_service[i];
if (!strcmp(var + 5, svc->config_name)) {
svc->enabled = git_config_bool(var, value);
return 0;
}
}
}
/* we are not interested in parsing any other configuration here */
return 0;
}
static struct rpc_service *select_service(const char *name)
{
struct rpc_service *svc = NULL;
int i;
if (prefixcmp(name, "git-"))
forbidden("Unsupported service: '%s'", name);
for (i = 0; i < ARRAY_SIZE(rpc_service); i++) {
struct rpc_service *s = &rpc_service[i];
if (!strcmp(s->name, name + 4)) {
svc = s;
break;
}
}
if (!svc)
forbidden("Unsupported service: '%s'", name);
if (svc->enabled < 0) {
const char *user = getenv("REMOTE_USER");
svc->enabled = (user && *user) ? 1 : 0;
}
if (!svc->enabled)
forbidden("Service not enabled: '%s'", svc->name);
return svc;
}
static void inflate_request(const char *prog_name, int out)
{
z_stream stream;
unsigned char in_buf[8192];
unsigned char out_buf[8192];
unsigned long cnt = 0;
int ret;
memset(&stream, 0, sizeof(stream));
ret = inflateInit2(&stream, (15 + 16));
if (ret != Z_OK)
die("cannot start zlib inflater, zlib err %d", ret);
while (1) {
ssize_t n = xread(0, in_buf, sizeof(in_buf));
if (n <= 0)
die("request ended in the middle of the gzip stream");
stream.next_in = in_buf;
stream.avail_in = n;
while (0 < stream.avail_in) {
int ret;
stream.next_out = out_buf;
stream.avail_out = sizeof(out_buf);
ret = inflate(&stream, Z_NO_FLUSH);
if (ret != Z_OK && ret != Z_STREAM_END)
die("zlib error inflating request, result %d", ret);
n = stream.total_out - cnt;
if (write_in_full(out, out_buf, n) != n)
die("%s aborted reading request", prog_name);
cnt += n;
if (ret == Z_STREAM_END)
goto done;
}
}
done:
inflateEnd(&stream);
close(out);
}
static void run_service(const char **argv)
{
const char *encoding = getenv("HTTP_CONTENT_ENCODING");
const char *user = getenv("REMOTE_USER");
const char *host = getenv("REMOTE_ADDR");
char *env[3];
struct strbuf buf = STRBUF_INIT;
int gzipped_request = 0;
struct child_process cld;
if (encoding && !strcmp(encoding, "gzip"))
gzipped_request = 1;
else if (encoding && !strcmp(encoding, "x-gzip"))
gzipped_request = 1;
if (!user || !*user)
user = "anonymous";
if (!host || !*host)
host = "(none)";
memset(&env, 0, sizeof(env));
strbuf_addf(&buf, "GIT_COMMITTER_NAME=%s", user);
env[0] = strbuf_detach(&buf, NULL);
strbuf_addf(&buf, "GIT_COMMITTER_EMAIL=%s@http.%s", user, host);
env[1] = strbuf_detach(&buf, NULL);
env[2] = NULL;
memset(&cld, 0, sizeof(cld));
cld.argv = argv;
cld.env = (const char *const *)env;
if (gzipped_request)
cld.in = -1;
cld.git_cmd = 1;
if (start_command(&cld))
exit(1);
close(1);
if (gzipped_request)
inflate_request(argv[0], cld.in);
else
close(0);
if (finish_command(&cld))
exit(1);
free(env[0]);
free(env[1]);
strbuf_release(&buf);
}
static int show_text_ref(const char *name, const unsigned char *sha1,
int flag, void *cb_data)
{
struct strbuf *buf = cb_data;
struct object *o = parse_object(sha1);
if (!o)
return 0;
strbuf_addf(buf, "%s\t%s\n", sha1_to_hex(sha1), name);
if (o->type == OBJ_TAG) {
o = deref_tag(o, name, 0);
if (!o)
return 0;
strbuf_addf(buf, "%s\t%s^{}\n", sha1_to_hex(o->sha1), name);
}
return 0;
}
static void get_info_refs(char *arg)
{
const char *service_name = get_parameter("service");
struct strbuf buf = STRBUF_INIT;
hdr_nocache();
if (service_name) {
const char *argv[] = {NULL /* service name */,
"--stateless-rpc", "--advertise-refs",
".", NULL};
struct rpc_service *svc = select_service(service_name);
strbuf_addf(&buf, "application/x-git-%s-advertisement",
svc->name);
hdr_str(content_type, buf.buf);
end_headers();
packet_write(1, "# service=git-%s\n", svc->name);
packet_flush(1);
argv[0] = svc->name;
run_service(argv);
} else {
select_getanyfile();
for_each_ref(show_text_ref, &buf);
send_strbuf("text/plain", &buf);
}
strbuf_release(&buf);
}
static void get_info_packs(char *arg)
{
size_t objdirlen = strlen(get_object_directory());
struct strbuf buf = STRBUF_INIT;
struct packed_git *p;
size_t cnt = 0;
select_getanyfile();
prepare_packed_git();
for (p = packed_git; p; p = p->next) {
if (p->pack_local)
cnt++;
}
strbuf_grow(&buf, cnt * 53 + 2);
for (p = packed_git; p; p = p->next) {
if (p->pack_local)
strbuf_addf(&buf, "P %s\n", p->pack_name + objdirlen + 6);
}
strbuf_addch(&buf, '\n');
hdr_nocache();
send_strbuf("text/plain; charset=utf-8", &buf);
strbuf_release(&buf);
}
static void check_content_type(const char *accepted_type)
{
const char *actual_type = getenv("CONTENT_TYPE");
if (!actual_type)
actual_type = "";
if (strcmp(actual_type, accepted_type)) {
http_status(415, "Unsupported Media Type");
hdr_nocache();
end_headers();
format_write(1,
"Expected POST with Content-Type '%s',"
" but received '%s' instead.\n",
accepted_type, actual_type);
exit(0);
}
}
static void service_rpc(char *service_name)
{
const char *argv[] = {NULL, "--stateless-rpc", ".", NULL};
struct rpc_service *svc = select_service(service_name);
struct strbuf buf = STRBUF_INIT;
strbuf_reset(&buf);
strbuf_addf(&buf, "application/x-git-%s-request", svc->name);
check_content_type(buf.buf);
hdr_nocache();
strbuf_reset(&buf);
strbuf_addf(&buf, "application/x-git-%s-result", svc->name);
hdr_str(content_type, buf.buf);
end_headers();
argv[0] = svc->name;
run_service(argv);
strbuf_release(&buf);
}
static NORETURN void die_webcgi(const char *err, va_list params)
{
static int dead;
if (!dead) {
dead = 1;
http_status(500, "Internal Server Error");
hdr_nocache();
end_headers();
vreportf("fatal: ", err, params);
}
exit(0); /* we successfully reported a failure ;-) */
}
static char* getdir(void)
{
struct strbuf buf = STRBUF_INIT;
char *pathinfo = getenv("PATH_INFO");
char *root = getenv("GIT_PROJECT_ROOT");
char *path = getenv("PATH_TRANSLATED");
if (root && *root) {
if (!pathinfo || !*pathinfo)
die("GIT_PROJECT_ROOT is set but PATH_INFO is not");
if (daemon_avoid_alias(pathinfo))
die("'%s': aliased", pathinfo);
strbuf_addstr(&buf, root);
if (buf.buf[buf.len - 1] != '/')
strbuf_addch(&buf, '/');
if (pathinfo[0] == '/')
pathinfo++;
strbuf_addstr(&buf, pathinfo);
return strbuf_detach(&buf, NULL);
} else if (path && *path) {
return xstrdup(path);
} else
die("No GIT_PROJECT_ROOT or PATH_TRANSLATED from server");
return NULL;
}
static struct service_cmd {
const char *method;
const char *pattern;
void (*imp)(char *);
} services[] = {
{"GET", "/HEAD$", get_text_file},
{"GET", "/info/refs$", get_info_refs},
{"GET", "/objects/info/alternates$", get_text_file},
{"GET", "/objects/info/http-alternates$", get_text_file},
{"GET", "/objects/info/packs$", get_info_packs},
{"GET", "/objects/[0-9a-f]{2}/[0-9a-f]{38}$", get_loose_object},
{"GET", "/objects/pack/pack-[0-9a-f]{40}\\.pack$", get_pack_file},
{"GET", "/objects/pack/pack-[0-9a-f]{40}\\.idx$", get_idx_file},
{"POST", "/git-upload-pack$", service_rpc},
{"POST", "/git-receive-pack$", service_rpc}
};
int main(int argc, char **argv)
{
char *method = getenv("REQUEST_METHOD");
char *dir;
struct service_cmd *cmd = NULL;
char *cmd_arg = NULL;
int i;
git_setup_gettext();
git_extract_argv0_path(argv[0]);
set_die_routine(die_webcgi);
if (!method)
die("No REQUEST_METHOD from server");
if (!strcmp(method, "HEAD"))
method = "GET";
dir = getdir();
for (i = 0; i < ARRAY_SIZE(services); i++) {
struct service_cmd *c = &services[i];
regex_t re;
regmatch_t out[1];
if (regcomp(&re, c->pattern, REG_EXTENDED))
die("Bogus regex in service table: %s", c->pattern);
if (!regexec(&re, dir, 1, out, 0)) {
size_t n;
if (strcmp(method, c->method)) {
const char *proto = getenv("SERVER_PROTOCOL");
if (proto && !strcmp(proto, "HTTP/1.1"))
http_status(405, "Method Not Allowed");
else
http_status(400, "Bad Request");
hdr_nocache();
end_headers();
return 0;
}
cmd = c;
n = out[0].rm_eo - out[0].rm_so;
cmd_arg = xmalloc(n);
memcpy(cmd_arg, dir + out[0].rm_so + 1, n-1);
cmd_arg[n-1] = '\0';
dir[out[0].rm_so] = 0;
break;
}
regfree(&re);
}
if (!cmd)
not_found("Request not supported: '%s'", dir);
setup_path();
if (!enter_repo(dir, 0))
not_found("Not a git repository: '%s'", dir);
if (!getenv("GIT_HTTP_EXPORT_ALL") &&
access("git-daemon-export-ok", F_OK) )
not_found("Repository not exported: '%s'", dir);
git_config(http_config, NULL);
cmd->imp(cmd_arg);
return 0;
}